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,4872 @@
1
+ const std = @import("std");
2
+ const assert = std.debug.assert;
3
+ const math = std.math;
4
+ const mem = std.mem;
5
+
6
+ const log = std.log.scoped(.state_machine);
7
+
8
+ const stdx = @import("stdx");
9
+ const maybe = stdx.maybe;
10
+
11
+ const constants = @import("constants.zig");
12
+ const tb = @import("tigerbeetle.zig");
13
+ const vsr = @import("vsr.zig");
14
+ const ScopeCloseMode = @import("lsm/tree.zig").ScopeCloseMode;
15
+ const WorkloadType = @import("state_machine/workload.zig").WorkloadType;
16
+ const GrooveType = @import("lsm/groove.zig").GrooveType;
17
+ const ForestType = @import("lsm/forest.zig").ForestType;
18
+ const ScanBuffer = @import("lsm/scan_buffer.zig").ScanBuffer;
19
+ const ScanLookupType = @import("lsm/scan_lookup.zig").ScanLookupType;
20
+
21
+ const MultiBatchEncoder = vsr.multi_batch.MultiBatchEncoder;
22
+ const MultiBatchDecoder = vsr.multi_batch.MultiBatchDecoder;
23
+
24
+ const Direction = @import("direction.zig").Direction;
25
+ const TimestampRange = @import("lsm/timestamp_range.zig").TimestampRange;
26
+
27
+ const Account = tb.Account;
28
+ const AccountFlags = tb.AccountFlags;
29
+ const AccountBalance = tb.AccountBalance;
30
+
31
+ const Transfer = tb.Transfer;
32
+ const TransferFlags = tb.TransferFlags;
33
+ const TransferPendingStatus = tb.TransferPendingStatus;
34
+
35
+ const CreateAccountResult = tb.CreateAccountResult;
36
+ const CreateTransferResult = tb.CreateTransferResult;
37
+
38
+ const AccountFilter = tb.AccountFilter;
39
+ const QueryFilter = tb.QueryFilter;
40
+ const ChangeEventsFilter = tb.ChangeEventsFilter;
41
+ const ChangeEvent = tb.ChangeEvent;
42
+ const ChangeEventType = tb.ChangeEventType;
43
+
44
+ pub const tree_ids = struct {
45
+ pub const Account = .{
46
+ .id = 1,
47
+ .user_data_128 = 2,
48
+ .user_data_64 = 3,
49
+ .user_data_32 = 4,
50
+ .ledger = 5,
51
+ .code = 6,
52
+ .timestamp = 7,
53
+ .imported = 23,
54
+ .closed = 25,
55
+ };
56
+
57
+ pub const Transfer = .{
58
+ .id = 8,
59
+ .debit_account_id = 9,
60
+ .credit_account_id = 10,
61
+ .amount = 11,
62
+ .pending_id = 12,
63
+ .user_data_128 = 13,
64
+ .user_data_64 = 14,
65
+ .user_data_32 = 15,
66
+ .ledger = 16,
67
+ .code = 17,
68
+ .timestamp = 18,
69
+ .expires_at = 19,
70
+ .imported = 24,
71
+ .closing = 26,
72
+ };
73
+
74
+ pub const TransferPending = .{
75
+ .timestamp = 20,
76
+ .status = 21,
77
+ };
78
+
79
+ pub const AccountEvents = .{
80
+ .timestamp = 22,
81
+ .account_timestamp = 27,
82
+ .transfer_pending_status = 28,
83
+ .dr_account_id_expired = 29,
84
+ .cr_account_id_expired = 30,
85
+ .transfer_pending_id_expired = 31,
86
+ .ledger_expired = 32,
87
+ .prunable = 33,
88
+ };
89
+ };
90
+
91
+ pub const TransferPending = extern struct {
92
+ timestamp: u64,
93
+ status: TransferPendingStatus,
94
+ padding: [7]u8 = @splat(0),
95
+
96
+ comptime {
97
+ // Assert that there is no implicit padding.
98
+ assert(@sizeOf(TransferPending) == 16);
99
+ assert(stdx.no_padding(TransferPending));
100
+ }
101
+ };
102
+
103
+ pub const AccountEvent = extern struct {
104
+ dr_account_id: u128,
105
+ dr_debits_pending: u128,
106
+ dr_debits_posted: u128,
107
+ dr_credits_pending: u128,
108
+ dr_credits_posted: u128,
109
+ cr_account_id: u128,
110
+ cr_debits_pending: u128,
111
+ cr_debits_posted: u128,
112
+ cr_credits_pending: u128,
113
+ cr_credits_posted: u128,
114
+ timestamp: u64,
115
+ dr_account_timestamp: u64,
116
+ cr_account_timestamp: u64,
117
+ dr_account_flags: AccountFlags,
118
+ cr_account_flags: AccountFlags,
119
+ transfer_flags: TransferFlags,
120
+ transfer_pending_flags: TransferFlags,
121
+ transfer_pending_id: u128,
122
+ amount_requested: u128,
123
+ amount: u128,
124
+ ledger: u32,
125
+
126
+ /// Although similar to `TransferPending.status`, this index tracks the event,
127
+ /// not the original pending transfer.
128
+ /// Examples: (No such index exists in `Transfers.flags`)
129
+ /// "All voided or expired events today."
130
+ /// "All single-phase or posted events today."
131
+ ///
132
+ /// Value | Description
133
+ /// ---------|-----------------------------------------------------
134
+ /// `none` | This event is a regular transfer.
135
+ /// `pending`| This event is a pending transfer.
136
+ /// `posted` | This event posted a pending transfer.
137
+ /// `voided` | This event voided a pending transfer.
138
+ /// `expired`| This event expired a pending transfer,
139
+ /// the `timestamp` does not relate to a transfer.
140
+ ///
141
+ /// See `transfer_pending_id` for tracking the pending transfer.
142
+ /// It will be `zero` for `none` and `pending`.
143
+ transfer_pending_status: TransferPendingStatus,
144
+ reserved: [11]u8 = @splat(0),
145
+
146
+ /// Previous schema before the changes introduced by PR #2507.
147
+ const Former = extern struct {
148
+ dr_account_id: u128,
149
+ dr_debits_pending: u128,
150
+ dr_debits_posted: u128,
151
+ dr_credits_pending: u128,
152
+ dr_credits_posted: u128,
153
+ cr_account_id: u128,
154
+ cr_debits_pending: u128,
155
+ cr_debits_posted: u128,
156
+ cr_credits_pending: u128,
157
+ cr_credits_posted: u128,
158
+ timestamp: u64,
159
+ reserved: [88]u8 = @splat(0),
160
+
161
+ comptime {
162
+ assert(stdx.no_padding(Former));
163
+ assert(@sizeOf(Former) == @sizeOf(AccountEvent));
164
+ assert(@alignOf(Former) == @alignOf(AccountEvent));
165
+
166
+ // Asserting the fields are identical.
167
+ for (std.meta.fields(Former)) |field_former| {
168
+ if (std.mem.eql(u8, field_former.name, "reserved")) continue;
169
+ const field = std.meta.fields(AccountEvent)[
170
+ std.meta.fieldIndex(
171
+ AccountEvent,
172
+ field_former.name,
173
+ ).?
174
+ ];
175
+ assert(field_former.type == field.type);
176
+ assert(field_former.alignment == field.alignment);
177
+ assert(@offsetOf(AccountEvent, field_former.name) ==
178
+ @offsetOf(Former, field_former.name));
179
+ }
180
+ }
181
+ };
182
+
183
+ /// Checks the object and returns whether it was created
184
+ /// using the current or the former schema.
185
+ fn schema(self: *const AccountEvent) union(enum) {
186
+ current,
187
+ former: *const AccountEvent.Former,
188
+ } {
189
+ assert(self.timestamp != 0);
190
+
191
+ const former: *const AccountEvent.Former = @ptrCast(self);
192
+ if (stdx.zeroed(&former.reserved)) {
193
+ // In the former schema:
194
+ // Balances for accounts without the `history` flag weren’t stored.
195
+ // If neither side had the `history` flag, the entire object wasn’t stored.
196
+ assert(former.dr_account_id != 0 or former.cr_account_id != 0);
197
+
198
+ return .{ .former = former };
199
+ }
200
+
201
+ assert(self.dr_account_timestamp != 0);
202
+ assert(self.dr_account_id != 0);
203
+ assert(self.cr_account_timestamp != 0);
204
+ assert(self.cr_account_id != 0);
205
+ assert(self.ledger != 0);
206
+ switch (self.transfer_pending_status) {
207
+ .none, .pending => assert(self.transfer_pending_id == 0),
208
+ .posted, .voided, .expired => assert(self.transfer_pending_id != 0),
209
+ }
210
+ assert(stdx.zeroed(&self.reserved));
211
+ return .current;
212
+ }
213
+
214
+ comptime {
215
+ assert(stdx.no_padding(AccountEvent));
216
+ assert(@sizeOf(AccountEvent) == 256);
217
+ assert(@alignOf(AccountEvent) == 16);
218
+ }
219
+ };
220
+
221
+ pub fn StateMachineType(comptime Storage: type) type {
222
+ assert(constants.message_body_size_max > 0);
223
+ assert(constants.lsm_compaction_ops > 0);
224
+ assert(constants.vsr_operations_reserved > 0);
225
+
226
+ return struct {
227
+ batch_size_limit: u32,
228
+ prefetch_timestamp: u64,
229
+ prepare_timestamp: u64,
230
+ commit_timestamp: u64,
231
+ forest: Forest,
232
+
233
+ prefetch_snapshot: ?u64 = null,
234
+ prefetch_operation: ?Operation = null,
235
+ prefetch_input: ?[]const u8 = null,
236
+ prefetch_callback: ?*const fn (*StateMachine) void = null,
237
+ prefetch_context: PrefetchContext = .null,
238
+
239
+ scan_lookup: ScanLookup = .null,
240
+ scan_lookup_buffer: []align(16) u8,
241
+ scan_lookup_buffer_index: u32 = 0,
242
+ scan_lookup_results: std.ArrayListUnmanaged(u32),
243
+ scan_lookup_next_tick: Grid.NextTick = undefined,
244
+
245
+ expire_pending_transfers: ExpirePendingTransfers = .{},
246
+
247
+ open_callback: ?*const fn (*StateMachine) void = null,
248
+ compact_callback: ?*const fn (*StateMachine) void = null,
249
+ checkpoint_callback: ?*const fn (*StateMachine) void = null,
250
+
251
+ /// Temporary metrics, until proper ones are merged.
252
+ metrics: Metrics,
253
+ log_trace: bool,
254
+
255
+ aof_recovery: bool,
256
+
257
+ const StateMachine = @This();
258
+ const Grid = @import("vsr/grid.zig").GridType(Storage);
259
+
260
+ /// Re-exports the `Contract` declarations, so it can be interchangeable
261
+ /// with a concrete state machine type.
262
+ pub const Operation = tb.Operation;
263
+
264
+ pub const Options = struct {
265
+ batch_size_limit: u32,
266
+ lsm_forest_compaction_block_count: u32,
267
+ lsm_forest_node_count: u32,
268
+ cache_entries_accounts: u32,
269
+ cache_entries_transfers: u32,
270
+ cache_entries_transfers_pending: u32,
271
+ log_trace: bool,
272
+ aof_recovery: bool,
273
+ };
274
+
275
+ pub const Workload = WorkloadType(StateMachine);
276
+
277
+ pub const Forest = ForestType(Storage, .{
278
+ .accounts = AccountsGroove,
279
+ .transfers = TransfersGroove,
280
+ .transfers_pending = TransfersPendingGroove,
281
+ .account_events = AccountEventsGroove,
282
+ });
283
+
284
+ pub const batch_max = struct {
285
+ pub const create_accounts: u32 = @max(
286
+ Operation.create_accounts.event_max(
287
+ constants.message_body_size_max,
288
+ ),
289
+ Operation.deprecated_create_accounts_unbatched.event_max(
290
+ constants.message_body_size_max,
291
+ ),
292
+ );
293
+ pub const create_transfers: u32 = @max(
294
+ Operation.create_transfers.event_max(
295
+ constants.message_body_size_max,
296
+ ),
297
+ Operation.deprecated_create_transfers_unbatched.event_max(
298
+ constants.message_body_size_max,
299
+ ),
300
+ );
301
+ pub const lookup_accounts: u32 = @max(
302
+ Operation.lookup_accounts.event_max(
303
+ constants.message_body_size_max,
304
+ ),
305
+ Operation.deprecated_lookup_accounts_unbatched.event_max(
306
+ constants.message_body_size_max,
307
+ ),
308
+ );
309
+ pub const lookup_transfers: u32 = @max(
310
+ Operation.lookup_transfers.event_max(
311
+ constants.message_body_size_max,
312
+ ),
313
+ Operation.deprecated_lookup_transfers_unbatched.event_max(
314
+ constants.message_body_size_max,
315
+ ),
316
+ );
317
+
318
+ comptime {
319
+ assert(create_accounts > 0);
320
+ assert(create_transfers > 0);
321
+ assert(lookup_accounts > 0);
322
+ assert(lookup_transfers > 0);
323
+ }
324
+ };
325
+
326
+ const tree_values_count_max = tree_values_count(constants.message_body_size_max);
327
+
328
+ const AccountsGroove = GrooveType(
329
+ Storage,
330
+ Account,
331
+ .{
332
+ .ids = tree_ids.Account,
333
+ .batch_value_count_max = tree_values_count_max.accounts,
334
+ .ignored = &[_][]const u8{
335
+ "debits_posted",
336
+ "debits_pending",
337
+ "credits_posted",
338
+ "credits_pending",
339
+ "flags",
340
+ "reserved",
341
+ },
342
+ .optional = &[_][]const u8{
343
+ "user_data_128",
344
+ "user_data_64",
345
+ "user_data_32",
346
+ },
347
+ .derived = .{
348
+ .imported = struct {
349
+ fn imported(object: *const Account) ?void {
350
+ return if (object.flags.imported) {} else null;
351
+ }
352
+ }.imported,
353
+ .closed = struct {
354
+ fn closed(object: *const Account) ?void {
355
+ return if (object.flags.closed) {} else null;
356
+ }
357
+ }.closed,
358
+ },
359
+ .orphaned_ids = false,
360
+ .objects_cache = true,
361
+ },
362
+ );
363
+
364
+ const TransfersGroove = GrooveType(
365
+ Storage,
366
+ Transfer,
367
+ .{
368
+ .ids = tree_ids.Transfer,
369
+ .batch_value_count_max = tree_values_count_max.transfers,
370
+ .ignored = &[_][]const u8{ "timeout", "flags" },
371
+ .optional = &[_][]const u8{
372
+ "pending_id",
373
+ "user_data_128",
374
+ "user_data_64",
375
+ "user_data_32",
376
+ },
377
+ .derived = .{
378
+ .expires_at = struct {
379
+ fn expires_at(object: *const Transfer) ?u64 {
380
+ if (object.flags.pending and object.timeout > 0) {
381
+ return object.timestamp + object.timeout_ns();
382
+ }
383
+ return null;
384
+ }
385
+ }.expires_at,
386
+ .imported = struct {
387
+ fn imported(object: *const Transfer) ?void {
388
+ return if (object.flags.imported) {} else null;
389
+ }
390
+ }.imported,
391
+ .closing = struct {
392
+ fn closing(object: *const Transfer) ?void {
393
+ if (object.flags.closing_debit or object.flags.closing_credit) {
394
+ return {};
395
+ } else {
396
+ return null;
397
+ }
398
+ }
399
+ }.closing,
400
+ },
401
+ .orphaned_ids = true,
402
+ .objects_cache = true,
403
+ },
404
+ );
405
+
406
+ const TransfersPendingGroove = GrooveType(
407
+ Storage,
408
+ TransferPending,
409
+ .{
410
+ .ids = tree_ids.TransferPending,
411
+ .batch_value_count_max = tree_values_count_max.transfers_pending,
412
+ .ignored = &[_][]const u8{"padding"},
413
+ .optional = &[_][]const u8{
414
+ // Index the current status of a pending transfer.
415
+ // Examples:
416
+ // "Pending transfers that are still pending."
417
+ // "Pending transfers that were voided or expired."
418
+ "status",
419
+ },
420
+ .derived = .{},
421
+ .orphaned_ids = false,
422
+ .objects_cache = true,
423
+ },
424
+ );
425
+
426
+ const AccountEventsGroove = GrooveType(
427
+ Storage,
428
+ AccountEvent,
429
+ .{
430
+ .ids = tree_ids.AccountEvents,
431
+ .batch_value_count_max = tree_values_count_max.account_events,
432
+ .ignored = &[_][]const u8{
433
+ "dr_account_id",
434
+ "dr_debits_pending",
435
+ "dr_debits_posted",
436
+ "dr_credits_pending",
437
+ "dr_credits_posted",
438
+ "cr_account_id",
439
+ "cr_debits_pending",
440
+ "cr_debits_posted",
441
+ "cr_credits_pending",
442
+ "cr_credits_posted",
443
+ "dr_account_timestamp",
444
+ "cr_account_timestamp",
445
+ "dr_account_flags",
446
+ "cr_account_flags",
447
+ "transfer_flags",
448
+ "transfer_pending_flags",
449
+ "transfer_pending_id",
450
+ "amount_requested",
451
+ "amount",
452
+ "ledger",
453
+ "reserved",
454
+ },
455
+ .optional = &[_][]const u8{},
456
+ .derived = .{
457
+ // Placeholder derived index (will be inserted during `account_event`).
458
+ //
459
+ // This index stores two values per object (the credit and debit accounts).
460
+ // It is used for balance as-of queries and to search events related to a
461
+ // particular account.
462
+ // Examples:
463
+ // "Balance where account=X and timestamp=Y".
464
+ // "Last time account=X was updated".
465
+ .account_timestamp = struct {
466
+ fn account_timestamp(_: *const AccountEvent) ?u64 {
467
+ return null;
468
+ }
469
+ }.account_timestamp,
470
+
471
+ // Events related to transfers can be searched using `Transfers.dr_account_id`
472
+ // and `Transfers.cr_account_id`.
473
+ // However, expired events require a specific index to be searchable by both
474
+ // debit and credit accounts.
475
+ // Example: "All expired debits where account=X".
476
+ .dr_account_id_expired = struct {
477
+ fn dr_account_id_expired(object: *const AccountEvent) ?u128 {
478
+ return if (object.transfer_pending_status == .expired)
479
+ object.dr_account_id
480
+ else
481
+ null;
482
+ }
483
+ }.dr_account_id_expired,
484
+ .cr_account_id_expired = struct {
485
+ fn cr_account_id_expired(object: *const AccountEvent) ?u128 {
486
+ return if (object.transfer_pending_status == .expired)
487
+ object.cr_account_id
488
+ else
489
+ null;
490
+ }
491
+ }.cr_account_id_expired,
492
+
493
+ // Events related to voiding or posting pending transfers can be searched using
494
+ // `Transfers.pending_id`.
495
+ // However, expired events require a specific index to be searchable by the
496
+ // transfer.
497
+ // Example: "When transfer=X has expired".
498
+ .transfer_pending_id_expired = struct {
499
+ fn transfer_pending_id_expired(object: *const AccountEvent) ?u128 {
500
+ return if (object.transfer_pending_status == .expired)
501
+ object.transfer_pending_id
502
+ else
503
+ null;
504
+ }
505
+ }.transfer_pending_id_expired,
506
+
507
+ // Events related to transfers can be searched using `Transfers.ledger`.
508
+ // However, expired events require a specific index to be searchable
509
+ // by ledger.
510
+ // Example: "All expiry events where ledger=X".
511
+ .ledger_expired = struct {
512
+ fn transfer_expired_ledger(object: *const AccountEvent) ?u128 {
513
+ return if (object.transfer_pending_status == .expired)
514
+ object.ledger
515
+ else
516
+ null;
517
+ }
518
+ }.transfer_expired_ledger,
519
+
520
+ // Tracks events for accounts without the history flag,
521
+ // enabling a cleanup job to delete them after CDC.
522
+ .prunable = struct {
523
+ fn prunable(object: *const AccountEvent) ?void {
524
+ if (object.dr_account_flags.history or
525
+ object.cr_account_flags.history)
526
+ {
527
+ return null;
528
+ } else {
529
+ return {};
530
+ }
531
+ }
532
+ }.prunable,
533
+ },
534
+ .orphaned_ids = false,
535
+ .objects_cache = false,
536
+ },
537
+ );
538
+
539
+ const AccountsScanLookup = ScanLookupType(
540
+ AccountsGroove,
541
+ AccountsGroove.ScanBuilder.Scan,
542
+ Storage,
543
+ );
544
+
545
+ const TransfersScanLookup = ScanLookupType(
546
+ TransfersGroove,
547
+ TransfersGroove.ScanBuilder.Scan,
548
+ Storage,
549
+ );
550
+
551
+ const AccountBalancesScanLookup = ScanLookupType(
552
+ AccountEventsGroove,
553
+ // Both Objects use the same timestamp, so we can use the TransfersGroove's indexes.
554
+ TransfersGroove.ScanBuilder.Scan,
555
+ Storage,
556
+ );
557
+
558
+ const ChangeEventsScanLookup = ChangeEventsScanLookupType(AccountEventsGroove, Storage);
559
+
560
+ /// Since prefetch contexts are used one at a time, it's safe to access
561
+ /// the union's fields and reuse the same memory for all context instances.
562
+ const PrefetchContext = union(enum) {
563
+ null,
564
+ accounts: AccountsGroove.PrefetchContext,
565
+ transfers: TransfersGroove.PrefetchContext,
566
+ transfers_pending: TransfersPendingGroove.PrefetchContext,
567
+
568
+ pub const Field = std.meta.FieldEnum(PrefetchContext);
569
+ pub fn FieldType(comptime field: Field) type {
570
+ return @FieldType(PrefetchContext, @tagName(field));
571
+ }
572
+
573
+ pub fn parent(
574
+ comptime field: Field,
575
+ completion: *FieldType(field),
576
+ ) *StateMachine {
577
+ comptime assert(field != .null);
578
+
579
+ const context: *PrefetchContext =
580
+ @alignCast(@fieldParentPtr(@tagName(field), completion));
581
+ return @alignCast(@fieldParentPtr("prefetch_context", context));
582
+ }
583
+
584
+ pub fn get(self: *PrefetchContext, comptime field: Field) *FieldType(field) {
585
+ comptime assert(field != .null);
586
+ assert(self.* == .null);
587
+
588
+ self.* = @unionInit(PrefetchContext, @tagName(field), undefined);
589
+ return &@field(self, @tagName(field));
590
+ }
591
+ };
592
+
593
+ const ExpirePendingTransfers = ExpirePendingTransfersType(TransfersGroove, Storage);
594
+
595
+ /// Since scan lookups are used one at a time, it's safe to access
596
+ /// the union's fields and reuse the same memory for all ScanLookup instances.
597
+ const ScanLookup = union(enum) {
598
+ null,
599
+ transfers: TransfersScanLookup,
600
+ accounts: AccountsScanLookup,
601
+ account_balances: AccountBalancesScanLookup,
602
+ expire_pending_transfers: ExpirePendingTransfers.ScanLookup,
603
+ change_events: ChangeEventsScanLookup,
604
+
605
+ pub const Field = std.meta.FieldEnum(ScanLookup);
606
+ pub fn FieldType(comptime field: Field) type {
607
+ return @FieldType(ScanLookup, @tagName(field));
608
+ }
609
+
610
+ pub fn parent(
611
+ comptime field: Field,
612
+ completion: *FieldType(field),
613
+ ) *StateMachine {
614
+ comptime assert(field != .null);
615
+
616
+ const context: *ScanLookup =
617
+ @alignCast(@fieldParentPtr(@tagName(field), completion));
618
+ return @alignCast(@fieldParentPtr("scan_lookup", context));
619
+ }
620
+
621
+ pub fn get(self: *ScanLookup, comptime field: Field) *FieldType(field) {
622
+ comptime assert(field != .null);
623
+ assert(self.* == .null);
624
+
625
+ self.* = @unionInit(ScanLookup, @tagName(field), undefined);
626
+ return &@field(self, @tagName(field));
627
+ }
628
+ };
629
+
630
+ const Metrics = struct {
631
+ create_accounts: TimingSummary = .{},
632
+ create_transfers: TimingSummary = .{},
633
+ lookup_accounts: TimingSummary = .{},
634
+ lookup_transfers: TimingSummary = .{},
635
+ get_account_transfers: TimingSummary = .{},
636
+ get_account_balances: TimingSummary = .{},
637
+ query_accounts: TimingSummary = .{},
638
+ query_transfers: TimingSummary = .{},
639
+ get_change_events: TimingSummary = .{},
640
+
641
+ compact: TimingSummary = .{},
642
+ checkpoint: TimingSummary = .{},
643
+
644
+ timer: vsr.time.Timer,
645
+
646
+ const TimingSummary = struct {
647
+ duration_min_us: ?u64 = null,
648
+ duration_max_us: ?u64 = null,
649
+ duration_sum_us: u64 = 0,
650
+
651
+ count: u64 = 0,
652
+
653
+ /// If an operation supports batching (eg, lookup_accounts) this is a count of the
654
+ /// number of internal operations.
655
+ count_batch: u64 = 0,
656
+ };
657
+
658
+ /// Technically 'timer' can't be used, but that'll error out at comptime.
659
+ const MetricEnum = std.meta.FieldEnum(Metrics);
660
+
661
+ pub fn log_and_reset(metrics: *Metrics) void {
662
+ inline for (comptime std.meta.fieldNames(Metrics)) |field_name| {
663
+ if (comptime !std.mem.eql(u8, field_name, "timer")) {
664
+ const timing: *TimingSummary = &@field(metrics, field_name);
665
+ if (timing.count > 0) {
666
+ log.info("{s}: p0={?}us mean={}us p100={?}us " ++
667
+ "sum={}us count={} count_batch={}", .{
668
+ field_name,
669
+ timing.duration_min_us,
670
+ @divFloor(timing.duration_sum_us, timing.count),
671
+ timing.duration_max_us,
672
+ timing.duration_sum_us,
673
+ timing.count,
674
+ timing.count_batch,
675
+ });
676
+ }
677
+ }
678
+ }
679
+
680
+ metrics.* = .{
681
+ .timer = metrics.timer,
682
+ };
683
+ }
684
+
685
+ pub fn record(
686
+ metrics: *Metrics,
687
+ comptime metric: MetricEnum,
688
+ duration_us: u64,
689
+ count_batch: u64,
690
+ ) void {
691
+ const timing: *Metrics.TimingSummary = &@field(
692
+ metrics,
693
+ @tagName(metric),
694
+ );
695
+
696
+ timing.duration_min_us = if (timing.duration_min_us) |duration_min_us|
697
+ @min(duration_min_us, duration_us)
698
+ else
699
+ duration_us;
700
+ timing.duration_max_us = if (timing.duration_max_us) |duration_max_us|
701
+ @max(duration_max_us, duration_us)
702
+ else
703
+ duration_us;
704
+ timing.duration_sum_us += duration_us;
705
+ timing.count += 1;
706
+ timing.count_batch += count_batch;
707
+ }
708
+
709
+ fn from_operation(comptime operation: Operation) MetricEnum {
710
+ return switch (operation) {
711
+ .create_accounts,
712
+ .deprecated_create_accounts_unbatched,
713
+ => .create_accounts,
714
+
715
+ .create_transfers,
716
+ .deprecated_create_transfers_unbatched,
717
+ => .create_transfers,
718
+
719
+ .lookup_accounts,
720
+ .deprecated_lookup_accounts_unbatched,
721
+ => .lookup_accounts,
722
+
723
+ .lookup_transfers,
724
+ .deprecated_lookup_transfers_unbatched,
725
+ => .lookup_transfers,
726
+
727
+ .get_account_transfers,
728
+ .deprecated_get_account_transfers_unbatched,
729
+ => .get_account_transfers,
730
+
731
+ .get_account_balances,
732
+ .deprecated_get_account_balances_unbatched,
733
+ => .get_account_balances,
734
+
735
+ .query_accounts,
736
+ .deprecated_query_accounts_unbatched,
737
+ => .query_accounts,
738
+
739
+ .query_transfers,
740
+ .deprecated_query_transfers_unbatched,
741
+ => .query_transfers,
742
+
743
+ .get_change_events,
744
+ => .get_change_events,
745
+
746
+ .pulse => comptime unreachable,
747
+ };
748
+ }
749
+ };
750
+
751
+ pub fn init(
752
+ self: *StateMachine,
753
+ allocator: mem.Allocator,
754
+ time: vsr.time.Time,
755
+ grid: *Grid,
756
+ options: Options,
757
+ ) !void {
758
+ assert(options.batch_size_limit <= constants.message_body_size_max);
759
+ inline for (comptime std.enums.values(Operation)) |operation| {
760
+ assert(options.batch_size_limit >= operation.event_size());
761
+ }
762
+
763
+ self.* = .{
764
+ .batch_size_limit = options.batch_size_limit,
765
+ .prefetch_snapshot = 0,
766
+ .prefetch_timestamp = 0,
767
+ .prepare_timestamp = 0,
768
+ .commit_timestamp = 0,
769
+
770
+ .forest = undefined,
771
+ .scan_lookup_buffer = undefined,
772
+ .scan_lookup_results = undefined,
773
+
774
+ .metrics = .{
775
+ .timer = .init(time),
776
+ },
777
+
778
+ .log_trace = options.log_trace,
779
+ .aof_recovery = options.aof_recovery,
780
+ };
781
+
782
+ try self.forest.init(
783
+ allocator,
784
+ grid,
785
+ .{
786
+ .compaction_block_count = options.lsm_forest_compaction_block_count,
787
+ .node_count = options.lsm_forest_node_count,
788
+ },
789
+ forest_options(options),
790
+ );
791
+ errdefer self.forest.deinit(allocator);
792
+
793
+ // The scan lookup buffer and the list that holds the result counts are shared between
794
+ // all operations, so they need to be large enough for the worst case.
795
+ const scan_lookup_buffer_size: usize, const scan_lookup_result_max: u16 =
796
+ max: {
797
+ const operations: []const Operation = &.{
798
+ .get_account_transfers,
799
+ .get_account_balances,
800
+ .query_accounts,
801
+ .query_transfers,
802
+ .get_change_events,
803
+
804
+ .deprecated_get_account_transfers_unbatched,
805
+ .deprecated_get_account_balances_unbatched,
806
+ .deprecated_query_accounts_unbatched,
807
+ .deprecated_query_transfers_unbatched,
808
+ };
809
+ var batch_count_max: u16 = 0;
810
+ var buffer_size_max: usize = 0;
811
+ inline for (operations) |operation| {
812
+ // The `Groove` object is stored in the buffer, not necessarily
813
+ // the same as `ResultType(operation)`.
814
+ const object_size: usize = switch (operation) {
815
+ .get_account_transfers,
816
+ .deprecated_get_account_transfers_unbatched,
817
+ => @sizeOf(Transfer),
818
+ .get_account_balances,
819
+ .deprecated_get_account_balances_unbatched,
820
+ => @sizeOf(AccountEvent),
821
+ .query_accounts,
822
+ .deprecated_query_accounts_unbatched,
823
+ => @sizeOf(Account),
824
+ .query_transfers,
825
+ .deprecated_query_transfers_unbatched,
826
+ => @sizeOf(Transfer),
827
+ .get_change_events => @sizeOf(AccountEvent),
828
+ else => comptime unreachable,
829
+ };
830
+ buffer_size_max = @max(
831
+ buffer_size_max,
832
+ operation.result_max(
833
+ self.batch_size_limit,
834
+ ) * object_size,
835
+ );
836
+
837
+ // For multi-batched queries, the result count of each individual query
838
+ // is stored in a list and used as the offset into `scan_lookup_buffer`.
839
+ batch_count_max = @max(
840
+ batch_count_max,
841
+ if (operation.is_multi_batch())
842
+ vsr.multi_batch.multi_batch_count_max(.{
843
+ .batch_size_min = operation.event_size(),
844
+ .batch_size_limit = options.batch_size_limit,
845
+ })
846
+ else
847
+ 1,
848
+ );
849
+ }
850
+ break :max .{ buffer_size_max, batch_count_max };
851
+ };
852
+ self.scan_lookup_buffer = try allocator.alignedAlloc(u8, 16, scan_lookup_buffer_size);
853
+ errdefer allocator.free(self.scan_lookup_buffer);
854
+
855
+ self.scan_lookup_results = try std.ArrayListUnmanaged(u32).initCapacity(
856
+ allocator,
857
+ scan_lookup_result_max,
858
+ );
859
+ errdefer self.scan_lookup_results.deinit(allocator);
860
+ }
861
+
862
+ pub fn deinit(self: *StateMachine, allocator: mem.Allocator) void {
863
+ allocator.free(self.scan_lookup_buffer);
864
+ self.scan_lookup_results.deinit(allocator);
865
+ self.forest.deinit(allocator);
866
+ }
867
+
868
+ pub fn reset(self: *StateMachine) void {
869
+ self.forest.reset();
870
+ self.scan_lookup_results.clearRetainingCapacity();
871
+
872
+ self.* = .{
873
+ .batch_size_limit = self.batch_size_limit,
874
+ .prefetch_timestamp = 0,
875
+ .prepare_timestamp = 0,
876
+ .commit_timestamp = 0,
877
+ .forest = self.forest,
878
+ .scan_lookup_buffer = self.scan_lookup_buffer,
879
+ .scan_lookup_results = self.scan_lookup_results,
880
+
881
+ .metrics = .{
882
+ .timer = self.metrics.timer,
883
+ },
884
+
885
+ .log_trace = self.log_trace,
886
+ .aof_recovery = self.aof_recovery,
887
+ };
888
+ }
889
+
890
+ pub fn open(self: *StateMachine, callback: *const fn (*StateMachine) void) void {
891
+ assert(self.open_callback == null);
892
+ self.open_callback = callback;
893
+
894
+ self.forest.open(forest_open_callback);
895
+ }
896
+
897
+ fn forest_open_callback(forest: *Forest) void {
898
+ const self: *StateMachine = @alignCast(@fieldParentPtr("forest", forest));
899
+ assert(self.open_callback != null);
900
+
901
+ const callback = self.open_callback.?;
902
+ self.open_callback = null;
903
+ callback(self);
904
+ }
905
+
906
+ pub fn input_valid(
907
+ self: *const StateMachine,
908
+ operation: Operation,
909
+ message_body_used: []align(16) const u8,
910
+ ) bool {
911
+ // NB: This function should never accept `client_release` as an argument.
912
+ // Any public API changes must be introduced explicitly as a new `operation` number.
913
+ assert(message_body_used.len <= self.batch_size_limit);
914
+
915
+ if (!operation.is_multi_batch()) {
916
+ return self.batch_valid(operation, message_body_used);
917
+ }
918
+ assert(operation.is_multi_batch());
919
+
920
+ const event_size: u32 = operation.event_size();
921
+ maybe(event_size == 0);
922
+ const result_size: u32 = operation.result_size();
923
+ assert(result_size > 0);
924
+
925
+ // Verifying whether the multi-batch message is properly encoded.
926
+ var body_decoder = MultiBatchDecoder.init(message_body_used, .{
927
+ .element_size = event_size,
928
+ }) catch |err| switch (err) {
929
+ error.MultiBatchInvalid => return false,
930
+ };
931
+
932
+ var result_count_expected: u32 = 0;
933
+ while (body_decoder.pop()) |batch| {
934
+ if (!self.batch_valid(operation, batch)) return false;
935
+ result_count_expected += operation.result_count_expected(batch);
936
+ }
937
+ const reply_trailer_size: u32 = vsr.multi_batch.trailer_total_size(.{
938
+ .element_size = result_size,
939
+ .batch_count = body_decoder.batch_count(),
940
+ });
941
+ // Checking if the expected number of results will fit the reply.
942
+ if (constants.message_body_size_max <
943
+ (result_count_expected * result_size) +
944
+ reply_trailer_size)
945
+ {
946
+ return false;
947
+ }
948
+
949
+ return true;
950
+ }
951
+
952
+ /// Validates a batch.
953
+ /// For multi-batch requests, this function expects a single, already decoded batch.
954
+ fn batch_valid(
955
+ self: *const StateMachine,
956
+ operation: Operation,
957
+ batch: []const u8,
958
+ ) bool {
959
+ assert(batch.len <= self.batch_size_limit);
960
+ maybe(batch.len == 0);
961
+ switch (operation) {
962
+ .pulse => return batch.len == 0,
963
+ inline else => |operation_comptime| {
964
+ const event_size = operation_comptime.event_size();
965
+ assert(event_size > 0);
966
+
967
+ if (comptime !operation_comptime.is_batchable()) {
968
+ return batch.len == event_size;
969
+ }
970
+ comptime assert(operation_comptime.is_batchable());
971
+
972
+ // Clients do not validate batch size == 0,
973
+ // and even the simulator can generate requests with no events.
974
+ maybe(batch.len == 0);
975
+ if (batch.len % event_size != 0) return false;
976
+
977
+ const event_max: u32 = operation_comptime.event_max(self.batch_size_limit);
978
+ assert(event_max > 0);
979
+
980
+ const event_count: u32 = @intCast(@divExact(batch.len, event_size));
981
+ if (event_count > event_max) return false;
982
+ return true;
983
+ },
984
+ }
985
+ }
986
+
987
+ /// Updates `prepare_timestamp` to the highest timestamp of the response.
988
+ pub fn prepare(
989
+ self: *StateMachine,
990
+ operation: Operation,
991
+ message_body_used: []align(16) const u8,
992
+ ) void {
993
+ assert(message_body_used.len <= self.batch_size_limit);
994
+ const delta: u64 = delta: {
995
+ if (!operation.is_multi_batch()) {
996
+ break :delta self.prepare_delta_nanoseconds(
997
+ operation,
998
+ message_body_used,
999
+ );
1000
+ }
1001
+ assert(operation.is_multi_batch());
1002
+
1003
+ var body_decoder = MultiBatchDecoder.init(message_body_used, .{
1004
+ .element_size = operation.event_size(),
1005
+ }) catch unreachable; // Already validated by `input_valid()`.
1006
+
1007
+ var delta: u64 = 0;
1008
+ while (body_decoder.pop()) |batch| {
1009
+ delta += self.prepare_delta_nanoseconds(
1010
+ operation,
1011
+ batch,
1012
+ );
1013
+ }
1014
+ break :delta delta;
1015
+ };
1016
+
1017
+ maybe(delta == 0);
1018
+ self.prepare_timestamp += delta;
1019
+ }
1020
+
1021
+ /// Returns the logical time increment (in nanoseconds) for the highest
1022
+ /// timestamp of the batch.
1023
+ /// For multi-batch requests, this function expects a single, already decoded batch.
1024
+ fn prepare_delta_nanoseconds(
1025
+ self: *StateMachine,
1026
+ operation: Operation,
1027
+ batch: []const u8,
1028
+ ) u64 {
1029
+ assert(batch.len <= self.batch_size_limit);
1030
+ return switch (operation) {
1031
+ .pulse => batch_max.create_transfers, // Max transfers to expire.
1032
+ .create_accounts => @divExact(batch.len, @sizeOf(Account)),
1033
+ .create_transfers => @divExact(batch.len, @sizeOf(Transfer)),
1034
+ .lookup_accounts => 0,
1035
+ .lookup_transfers => 0,
1036
+ .get_account_transfers => 0,
1037
+ .get_account_balances => 0,
1038
+ .query_accounts => 0,
1039
+ .query_transfers => 0,
1040
+ .get_change_events => 0,
1041
+
1042
+ .deprecated_create_accounts_unbatched => @divExact(batch.len, @sizeOf(Account)),
1043
+ .deprecated_create_transfers_unbatched => @divExact(batch.len, @sizeOf(Transfer)),
1044
+ .deprecated_lookup_accounts_unbatched => 0,
1045
+ .deprecated_lookup_transfers_unbatched => 0,
1046
+ .deprecated_get_account_transfers_unbatched => 0,
1047
+ .deprecated_get_account_balances_unbatched => 0,
1048
+ .deprecated_query_accounts_unbatched => 0,
1049
+ .deprecated_query_transfers_unbatched => 0,
1050
+ };
1051
+ }
1052
+
1053
+ pub fn pulse_needed(self: *const StateMachine, timestamp: u64) bool {
1054
+ assert(!self.aof_recovery);
1055
+ assert(self.expire_pending_transfers.pulse_next_timestamp >=
1056
+ TimestampRange.timestamp_min);
1057
+
1058
+ return self.expire_pending_transfers.pulse_next_timestamp <= timestamp;
1059
+ }
1060
+
1061
+ pub fn prefetch(
1062
+ self: *StateMachine,
1063
+ callback: *const fn (*StateMachine) void,
1064
+ op: u64,
1065
+ snapshot: u64,
1066
+ operation: Operation,
1067
+ message_body_used: []align(16) const u8,
1068
+ ) void {
1069
+ // NB: This function should never accept `client_release` as an argument.
1070
+ // Any public API changes must be introduced explicitly as a new `operation` number.
1071
+ assert(op > 0);
1072
+ assert(op <= snapshot);
1073
+ assert(self.prefetch_operation == null);
1074
+ assert(self.prefetch_input == null);
1075
+ assert(self.prefetch_callback == null);
1076
+ assert(message_body_used.len <= self.batch_size_limit);
1077
+
1078
+ assert(self.scan_lookup_buffer_index == 0);
1079
+ assert(self.scan_lookup_results.items.len == 0);
1080
+
1081
+ const prefetch_input: []const u8 = input: {
1082
+ if (!operation.is_multi_batch()) {
1083
+ assert(self.batch_valid(operation, message_body_used));
1084
+ break :input message_body_used;
1085
+ }
1086
+ assert(operation.is_multi_batch());
1087
+
1088
+ var body_decoder = MultiBatchDecoder.init(
1089
+ message_body_used,
1090
+ .{
1091
+ .element_size = operation.event_size(),
1092
+ },
1093
+ ) catch unreachable; // Already validated by `input_valid()`.
1094
+ while (body_decoder.pop()) |input| {
1095
+ assert(self.batch_valid(operation, input));
1096
+ }
1097
+ break :input body_decoder.payload;
1098
+ };
1099
+
1100
+ self.prefetch_snapshot = snapshot;
1101
+ self.prefetch_operation = operation;
1102
+ self.prefetch_input = prefetch_input;
1103
+ self.prefetch_callback = callback;
1104
+
1105
+ // TODO(Snapshots) Pass in the target snapshot.
1106
+ self.forest.grooves.accounts.prefetch_setup(snapshot);
1107
+ self.forest.grooves.transfers.prefetch_setup(snapshot);
1108
+ self.forest.grooves.transfers_pending.prefetch_setup(snapshot);
1109
+
1110
+ // Prefetch starts timing for an operation.
1111
+ self.metrics.timer.reset();
1112
+
1113
+ switch (operation) {
1114
+ .pulse => self.prefetch_expire_pending_transfers(),
1115
+ .create_accounts => self.prefetch_create_accounts(),
1116
+ .create_transfers => self.prefetch_create_transfers(),
1117
+ .lookup_accounts => self.prefetch_lookup_accounts(),
1118
+ .lookup_transfers => self.prefetch_lookup_transfers(),
1119
+ .get_account_transfers => self.prefetch_get_account_transfers(),
1120
+ .get_account_balances => self.prefetch_get_account_balances(),
1121
+ .query_accounts => self.prefetch_query_accounts(),
1122
+ .query_transfers => self.prefetch_query_transfers(),
1123
+ .get_change_events => self.prefetch_get_change_events(),
1124
+
1125
+ .deprecated_create_accounts_unbatched => {
1126
+ self.prefetch_create_accounts();
1127
+ },
1128
+ .deprecated_create_transfers_unbatched => {
1129
+ self.prefetch_create_transfers();
1130
+ },
1131
+ .deprecated_lookup_accounts_unbatched => {
1132
+ self.prefetch_lookup_accounts();
1133
+ },
1134
+ .deprecated_lookup_transfers_unbatched => {
1135
+ self.prefetch_lookup_transfers();
1136
+ },
1137
+ .deprecated_get_account_transfers_unbatched => {
1138
+ self.prefetch_get_account_transfers();
1139
+ },
1140
+ .deprecated_get_account_balances_unbatched => {
1141
+ self.prefetch_get_account_balances();
1142
+ },
1143
+ .deprecated_query_accounts_unbatched => {
1144
+ self.prefetch_query_accounts();
1145
+ },
1146
+ .deprecated_query_transfers_unbatched => {
1147
+ self.prefetch_query_transfers();
1148
+ },
1149
+ }
1150
+ }
1151
+
1152
+ fn prefetch_finish(self: *StateMachine) void {
1153
+ assert(self.prefetch_operation != null);
1154
+ assert(self.prefetch_input != null);
1155
+ assert(self.prefetch_snapshot != null);
1156
+ assert(self.prefetch_context == .null);
1157
+ assert(self.scan_lookup == .null);
1158
+
1159
+ const callback = self.prefetch_callback.?;
1160
+ self.prefetch_callback = null;
1161
+ self.prefetch_snapshot = null;
1162
+ self.prefetch_operation = null;
1163
+ self.prefetch_input = null;
1164
+
1165
+ callback(self);
1166
+ }
1167
+
1168
+ fn prefetch_create_accounts(self: *StateMachine) void {
1169
+ assert(self.prefetch_input != null);
1170
+ assert(self.prefetch_operation == .create_accounts or
1171
+ self.prefetch_operation == .deprecated_create_accounts_unbatched);
1172
+
1173
+ const accounts = stdx.bytes_as_slice(
1174
+ .exact,
1175
+ Account,
1176
+ self.prefetch_input.?,
1177
+ );
1178
+ for (accounts) |*a| {
1179
+ self.forest.grooves.accounts.prefetch_enqueue(a.id);
1180
+ }
1181
+
1182
+ self.forest.grooves.accounts.prefetch(
1183
+ prefetch_create_accounts_callback,
1184
+ self.prefetch_context.get(.accounts),
1185
+ );
1186
+ }
1187
+
1188
+ fn prefetch_create_accounts_callback(
1189
+ completion: *AccountsGroove.PrefetchContext,
1190
+ ) void {
1191
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
1192
+ assert(self.prefetch_input != null);
1193
+ assert(self.prefetch_operation == .create_accounts or
1194
+ self.prefetch_operation == .deprecated_create_accounts_unbatched);
1195
+
1196
+ self.prefetch_context = .null;
1197
+ const accounts = stdx.bytes_as_slice(
1198
+ .exact,
1199
+ Account,
1200
+ self.prefetch_input.?,
1201
+ );
1202
+ if (accounts.len > 0 and
1203
+ accounts[0].flags.imported)
1204
+ {
1205
+ // Looking for transfers with the same timestamp.
1206
+ for (accounts) |*a| {
1207
+ self.forest.grooves.transfers.prefetch_enqueue_by_timestamp(a.timestamp);
1208
+ }
1209
+
1210
+ self.forest.grooves.transfers.prefetch(
1211
+ prefetch_create_accounts_transfers_callback,
1212
+ self.prefetch_context.get(.transfers),
1213
+ );
1214
+ } else {
1215
+ self.prefetch_finish();
1216
+ }
1217
+ }
1218
+
1219
+ fn prefetch_create_accounts_transfers_callback(
1220
+ completion: *TransfersGroove.PrefetchContext,
1221
+ ) void {
1222
+ const self: *StateMachine = PrefetchContext.parent(.transfers, completion);
1223
+ assert(self.prefetch_input != null);
1224
+ assert(self.prefetch_operation == .create_accounts or
1225
+ self.prefetch_operation == .deprecated_create_accounts_unbatched);
1226
+
1227
+ self.prefetch_context = .null;
1228
+ self.prefetch_finish();
1229
+ }
1230
+
1231
+ fn prefetch_create_transfers(self: *StateMachine) void {
1232
+ assert(self.prefetch_input != null);
1233
+ assert(self.prefetch_operation == .create_transfers or
1234
+ self.prefetch_operation == .deprecated_create_transfers_unbatched);
1235
+
1236
+ const transfers = stdx.bytes_as_slice(
1237
+ .exact,
1238
+ Transfer,
1239
+ self.prefetch_input.?,
1240
+ );
1241
+ for (transfers) |*t| {
1242
+ self.forest.grooves.transfers.prefetch_enqueue(t.id);
1243
+
1244
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
1245
+ self.forest.grooves.transfers.prefetch_enqueue(t.pending_id);
1246
+ }
1247
+ }
1248
+
1249
+ self.forest.grooves.transfers.prefetch(
1250
+ prefetch_create_transfers_callback_transfers,
1251
+ self.prefetch_context.get(.transfers),
1252
+ );
1253
+ }
1254
+
1255
+ fn prefetch_create_transfers_callback_transfers(
1256
+ completion: *TransfersGroove.PrefetchContext,
1257
+ ) void {
1258
+ const self: *StateMachine = PrefetchContext.parent(.transfers, completion);
1259
+ assert(self.prefetch_input != null);
1260
+ assert(self.prefetch_operation == .create_transfers or
1261
+ self.prefetch_operation == .deprecated_create_transfers_unbatched);
1262
+
1263
+ self.prefetch_context = .null;
1264
+ const transfers = stdx.bytes_as_slice(
1265
+ .exact,
1266
+ Transfer,
1267
+ self.prefetch_input.?,
1268
+ );
1269
+ for (transfers) |*t| {
1270
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
1271
+ if (self.get_transfer(t.pending_id)) |p| {
1272
+ // This prefetch isn't run yet, but enqueue it here as well to save an extra
1273
+ // iteration over transfers.
1274
+ self.forest.grooves.transfers_pending.prefetch_enqueue(p.timestamp);
1275
+
1276
+ self.forest.grooves.accounts.prefetch_enqueue(p.debit_account_id);
1277
+ self.forest.grooves.accounts.prefetch_enqueue(p.credit_account_id);
1278
+ }
1279
+ } else {
1280
+ self.forest.grooves.accounts.prefetch_enqueue(t.debit_account_id);
1281
+ self.forest.grooves.accounts.prefetch_enqueue(t.credit_account_id);
1282
+ }
1283
+ }
1284
+
1285
+ if (transfers.len > 0 and
1286
+ transfers[0].flags.imported)
1287
+ {
1288
+ // Looking for accounts with the same timestamp.
1289
+ // This logic could be in the loop above, but we choose to iterate again,
1290
+ // avoiding an extra comparison in the more common case of non-imported batches.
1291
+ for (transfers) |*t| {
1292
+ self.forest.grooves.accounts.prefetch_enqueue_by_timestamp(t.timestamp);
1293
+ }
1294
+ }
1295
+
1296
+ self.forest.grooves.accounts.prefetch(
1297
+ prefetch_create_transfers_callback_accounts,
1298
+ self.prefetch_context.get(.accounts),
1299
+ );
1300
+ }
1301
+
1302
+ fn prefetch_create_transfers_callback_accounts(
1303
+ completion: *AccountsGroove.PrefetchContext,
1304
+ ) void {
1305
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
1306
+ assert(self.prefetch_input != null);
1307
+ assert(self.prefetch_operation == .create_transfers or
1308
+ self.prefetch_operation == .deprecated_create_transfers_unbatched);
1309
+
1310
+ self.prefetch_context = .null;
1311
+ self.forest.grooves.transfers_pending.prefetch(
1312
+ prefetch_create_transfers_callback_transfers_pending,
1313
+ self.prefetch_context.get(.transfers_pending),
1314
+ );
1315
+ }
1316
+
1317
+ fn prefetch_create_transfers_callback_transfers_pending(
1318
+ completion: *TransfersPendingGroove.PrefetchContext,
1319
+ ) void {
1320
+ const self: *StateMachine = PrefetchContext.parent(.transfers_pending, completion);
1321
+ assert(self.prefetch_input != null);
1322
+ assert(self.prefetch_operation == .create_transfers or
1323
+ self.prefetch_operation == .deprecated_create_transfers_unbatched);
1324
+
1325
+ self.prefetch_context = .null;
1326
+ self.prefetch_finish();
1327
+ }
1328
+
1329
+ fn prefetch_lookup_accounts(self: *StateMachine) void {
1330
+ assert(self.prefetch_input != null);
1331
+ assert(self.prefetch_operation == .lookup_accounts or
1332
+ self.prefetch_operation == .deprecated_lookup_accounts_unbatched);
1333
+
1334
+ const ids = stdx.bytes_as_slice(
1335
+ .exact,
1336
+ u128,
1337
+ self.prefetch_input.?,
1338
+ );
1339
+ for (ids) |id| {
1340
+ self.forest.grooves.accounts.prefetch_enqueue(id);
1341
+ }
1342
+
1343
+ self.forest.grooves.accounts.prefetch(
1344
+ prefetch_lookup_accounts_callback,
1345
+ self.prefetch_context.get(.accounts),
1346
+ );
1347
+ }
1348
+
1349
+ fn prefetch_lookup_accounts_callback(completion: *AccountsGroove.PrefetchContext) void {
1350
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
1351
+ assert(self.prefetch_input != null);
1352
+ assert(self.prefetch_operation == .lookup_accounts or
1353
+ self.prefetch_operation == .deprecated_lookup_accounts_unbatched);
1354
+
1355
+ self.prefetch_context = .null;
1356
+ self.prefetch_finish();
1357
+ }
1358
+
1359
+ fn prefetch_lookup_transfers(self: *StateMachine) void {
1360
+ assert(self.prefetch_input != null);
1361
+ assert(self.prefetch_operation == .lookup_transfers or
1362
+ self.prefetch_operation == .deprecated_lookup_transfers_unbatched);
1363
+
1364
+ const ids = stdx.bytes_as_slice(
1365
+ .exact,
1366
+ u128,
1367
+ self.prefetch_input.?,
1368
+ );
1369
+ for (ids) |id| {
1370
+ self.forest.grooves.transfers.prefetch_enqueue(id);
1371
+ }
1372
+
1373
+ self.forest.grooves.transfers.prefetch(
1374
+ prefetch_lookup_transfers_callback,
1375
+ self.prefetch_context.get(.transfers),
1376
+ );
1377
+ }
1378
+
1379
+ fn prefetch_lookup_transfers_callback(completion: *TransfersGroove.PrefetchContext) void {
1380
+ const self: *StateMachine = PrefetchContext.parent(.transfers, completion);
1381
+ assert(self.prefetch_input != null);
1382
+ assert(self.prefetch_operation == .lookup_transfers or
1383
+ self.prefetch_operation == .deprecated_lookup_transfers_unbatched);
1384
+
1385
+ self.prefetch_context = .null;
1386
+ self.prefetch_finish();
1387
+ }
1388
+
1389
+ fn prefetch_get_account_transfers(self: *StateMachine) void {
1390
+ assert(self.prefetch_input != null);
1391
+ assert(self.prefetch_operation.? == .get_account_transfers or
1392
+ self.prefetch_operation.? == .deprecated_get_account_transfers_unbatched);
1393
+ assert(self.scan_lookup == .null);
1394
+ assert(self.scan_lookup_buffer_index == 0);
1395
+ assert(self.scan_lookup_results.items.len == 0);
1396
+
1397
+ const filter: *const AccountFilter = self.get_prefetch_account_filter().?;
1398
+ self.prefetch_get_account_transfers_scan(filter);
1399
+ }
1400
+
1401
+ fn prefetch_get_account_transfers_scan(
1402
+ self: *StateMachine,
1403
+ filter: *const AccountFilter,
1404
+ ) void {
1405
+ assert(self.prefetch_input != null);
1406
+ assert(self.prefetch_operation.? == .get_account_transfers or
1407
+ self.prefetch_operation.? == .deprecated_get_account_transfers_unbatched);
1408
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1409
+ maybe(self.scan_lookup_results.items.len > 0);
1410
+
1411
+ log.debug("{?}: get_account_transfers: {}", .{
1412
+ self.forest.grid.superblock.replica_index,
1413
+ filter,
1414
+ });
1415
+
1416
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1417
+ if (self.get_scan_from_account_filter(filter)) |scan| {
1418
+ assert(self.forest.scan_buffer_pool.scan_buffer_used > 0);
1419
+
1420
+ const scan_buffer = stdx.bytes_as_slice(
1421
+ .inexact,
1422
+ Transfer,
1423
+ self.scan_lookup_buffer[self.scan_lookup_buffer_index..],
1424
+ );
1425
+
1426
+ const scan_lookup = self.scan_lookup.get(.transfers);
1427
+ scan_lookup.* = TransfersScanLookup.init(
1428
+ &self.forest.grooves.transfers,
1429
+ scan,
1430
+ );
1431
+
1432
+ // Limiting the buffer size according to the query limit.
1433
+ // TODO: Prevent clients from setting the limit larger than the buffer size.
1434
+ const limit = @min(
1435
+ filter.limit,
1436
+ self.prefetch_operation.?.result_max(self.batch_size_limit),
1437
+ );
1438
+ assert(limit > 0);
1439
+ assert(scan_buffer.len >= limit);
1440
+ scan_lookup.read(
1441
+ scan_buffer[0..limit],
1442
+ &prefetch_get_account_transfers_scan_callback,
1443
+ );
1444
+ return;
1445
+ }
1446
+
1447
+ // TODO(batiati): Improve the way we do validations on the state machine.
1448
+ log.info("invalid filter for get_account_transfers: {any}", .{filter});
1449
+ self.forest.grid.on_next_tick(
1450
+ &prefetch_scan_next_tick_callback,
1451
+ &self.scan_lookup_next_tick,
1452
+ );
1453
+ }
1454
+
1455
+ fn prefetch_get_account_transfers_scan_callback(
1456
+ scan_lookup: *TransfersScanLookup,
1457
+ results: []const Transfer,
1458
+ ) void {
1459
+ const self: *StateMachine = ScanLookup.parent(.transfers, scan_lookup);
1460
+ assert(self.prefetch_input != null);
1461
+ assert(self.prefetch_operation.? == .get_account_transfers or
1462
+ self.prefetch_operation.? == .deprecated_get_account_transfers_unbatched);
1463
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1464
+ maybe(self.scan_lookup_results.items.len > 0);
1465
+
1466
+ self.scan_lookup_buffer_index += @intCast(results.len * @sizeOf(Transfer));
1467
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
1468
+
1469
+ self.scan_lookup = .null;
1470
+ self.forest.scan_buffer_pool.reset();
1471
+ self.forest.grooves.transfers.scan_builder.reset();
1472
+
1473
+ return self.prefetch_scan_resume();
1474
+ }
1475
+
1476
+ fn prefetch_get_account_balances(self: *StateMachine) void {
1477
+ assert(self.prefetch_input != null);
1478
+ assert(self.prefetch_operation.? == .get_account_balances or
1479
+ self.prefetch_operation.? == .deprecated_get_account_balances_unbatched);
1480
+ assert(self.scan_lookup == .null);
1481
+ assert(self.scan_lookup_buffer_index == 0);
1482
+ assert(self.scan_lookup_results.items.len == 0);
1483
+
1484
+ const filters = stdx.bytes_as_slice(
1485
+ .exact,
1486
+ AccountFilter,
1487
+ self.prefetch_input.?,
1488
+ );
1489
+ assert(filters.len > 0);
1490
+ assert(filters.len == 1 or self.prefetch_operation.?.is_multi_batch());
1491
+ for (filters) |*filter| {
1492
+ self.forest.grooves.accounts.prefetch_enqueue(filter.account_id);
1493
+ }
1494
+ self.forest.grooves.accounts.prefetch(
1495
+ prefetch_get_account_balances_lookup_account_callback,
1496
+ self.prefetch_context.get(.accounts),
1497
+ );
1498
+ }
1499
+
1500
+ fn prefetch_get_account_balances_lookup_account_callback(
1501
+ completion: *AccountsGroove.PrefetchContext,
1502
+ ) void {
1503
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
1504
+ assert(self.prefetch_input != null);
1505
+ assert(self.prefetch_operation.? == .get_account_balances or
1506
+ self.prefetch_operation.? == .deprecated_get_account_balances_unbatched);
1507
+ assert(self.scan_lookup == .null);
1508
+ assert(self.scan_lookup_buffer_index == 0);
1509
+ assert(self.scan_lookup_results.items.len == 0);
1510
+
1511
+ self.prefetch_context = .null;
1512
+ const filter: *const AccountFilter = self.get_prefetch_account_filter().?;
1513
+ self.prefetch_get_account_balances_scan(filter);
1514
+ }
1515
+
1516
+ fn prefetch_get_account_balances_scan(
1517
+ self: *StateMachine,
1518
+ filter: *const AccountFilter,
1519
+ ) void {
1520
+ assert(self.prefetch_input != null);
1521
+ assert(self.prefetch_operation.? == .get_account_balances or
1522
+ self.prefetch_operation.? == .deprecated_get_account_balances_unbatched);
1523
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1524
+ maybe(self.scan_lookup_results.items.len > 0);
1525
+
1526
+ log.debug("{?}: get_account_balances: {}", .{
1527
+ self.forest.grid.superblock.replica_index,
1528
+ filter,
1529
+ });
1530
+
1531
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1532
+ if (self.get_account(filter.account_id)) |account| {
1533
+ if (account.flags.history) {
1534
+ if (self.get_scan_from_account_filter(filter)) |scan| {
1535
+ assert(self.forest.scan_buffer_pool.scan_buffer_used > 0);
1536
+
1537
+ const scan_buffer = stdx.bytes_as_slice(
1538
+ .inexact,
1539
+ AccountEvent,
1540
+ self.scan_lookup_buffer[self.scan_lookup_buffer_index..],
1541
+ );
1542
+
1543
+ const scan_lookup = self.scan_lookup.get(.account_balances);
1544
+ scan_lookup.* = AccountBalancesScanLookup.init(
1545
+ &self.forest.grooves.account_events,
1546
+ scan,
1547
+ );
1548
+
1549
+ // Limiting the buffer size according to the query limit.
1550
+ // TODO: Prevent clients from setting the limit larger than the buffer size.
1551
+ const limit = @min(
1552
+ filter.limit,
1553
+ self.prefetch_operation.?.result_max(self.batch_size_limit),
1554
+ );
1555
+ assert(limit > 0);
1556
+ assert(scan_buffer.len >= limit);
1557
+ scan_lookup.read(
1558
+ scan_buffer[0..limit],
1559
+ &prefetch_get_account_balances_scan_callback,
1560
+ );
1561
+ return;
1562
+ } else {
1563
+ // TODO(batiati): Improve the way we do validations on the state machine.
1564
+ log.info("get_account_balances: invalid filter: {any}", .{filter});
1565
+ }
1566
+ } else {
1567
+ log.info(
1568
+ "get_account_balances: cannot query account.id={}; flags.history=false",
1569
+ .{filter.account_id},
1570
+ );
1571
+ }
1572
+ } else {
1573
+ log.info(
1574
+ "get_account_balances: cannot query account.id={}; account does not exist",
1575
+ .{filter.account_id},
1576
+ );
1577
+ }
1578
+
1579
+ // Returning an empty array on the next tick.
1580
+ self.forest.grid.on_next_tick(
1581
+ &prefetch_scan_next_tick_callback,
1582
+ &self.scan_lookup_next_tick,
1583
+ );
1584
+ }
1585
+
1586
+ fn prefetch_get_account_balances_scan_callback(
1587
+ scan_lookup: *AccountBalancesScanLookup,
1588
+ results: []const AccountEvent,
1589
+ ) void {
1590
+ const self: *StateMachine = ScanLookup.parent(.account_balances, scan_lookup);
1591
+ assert(self.prefetch_input != null);
1592
+ assert(self.prefetch_operation.? == .get_account_balances or
1593
+ self.prefetch_operation.? == .deprecated_get_account_balances_unbatched);
1594
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1595
+ maybe(self.scan_lookup_results.items.len > 0);
1596
+
1597
+ self.scan_lookup_buffer_index += @intCast(results.len * @sizeOf(AccountEvent));
1598
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
1599
+
1600
+ self.forest.scan_buffer_pool.reset();
1601
+ self.forest.grooves.transfers.scan_builder.reset();
1602
+ self.scan_lookup = .null;
1603
+
1604
+ return self.prefetch_scan_resume();
1605
+ }
1606
+
1607
+ /// Returns the `AccountFilter` from the prefetch input buffer.
1608
+ /// In the case of multi-batch inputs, returns the current filter
1609
+ /// or `null` if all filters have been executed.
1610
+ fn get_prefetch_account_filter(self: *StateMachine) ?*const AccountFilter {
1611
+ assert(self.prefetch_input != null);
1612
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
1613
+
1614
+ switch (self.prefetch_operation.?) {
1615
+ .get_account_transfers, .get_account_balances => {
1616
+ const filter_index = self.scan_lookup_results.items.len;
1617
+ maybe(filter_index > 0);
1618
+
1619
+ const filters = stdx.bytes_as_slice(
1620
+ .exact,
1621
+ AccountFilter,
1622
+ self.prefetch_input.?,
1623
+ );
1624
+ assert(filters.len > 0);
1625
+ assert(filter_index <= filters.len);
1626
+
1627
+ // Returns null if all filters were processed.
1628
+ if (filter_index == filters.len) return null;
1629
+ return &filters[filter_index];
1630
+ },
1631
+ .deprecated_get_account_transfers_unbatched,
1632
+ .deprecated_get_account_balances_unbatched,
1633
+ => {
1634
+ // Operations not encoded as multi-batch must have only a single filter.
1635
+ assert(self.scan_lookup_results.items.len == 0);
1636
+ const filter: *const AccountFilter = @alignCast(std.mem.bytesAsValue(
1637
+ AccountFilter,
1638
+ self.prefetch_input.?,
1639
+ ));
1640
+ return filter;
1641
+ },
1642
+ else => unreachable,
1643
+ }
1644
+ }
1645
+
1646
+ fn get_scan_from_account_filter(
1647
+ self: *StateMachine,
1648
+ filter: *const AccountFilter,
1649
+ ) ?*TransfersGroove.ScanBuilder.Scan {
1650
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1651
+
1652
+ const filter_valid =
1653
+ filter.account_id != 0 and filter.account_id != std.math.maxInt(u128) and
1654
+ (filter.timestamp_min == 0 or TimestampRange.valid(filter.timestamp_min)) and
1655
+ (filter.timestamp_max == 0 or TimestampRange.valid(filter.timestamp_max)) and
1656
+ (filter.timestamp_max == 0 or filter.timestamp_min <= filter.timestamp_max) and
1657
+ filter.limit != 0 and
1658
+ (filter.flags.credits or filter.flags.debits) and
1659
+ filter.flags.padding == 0 and
1660
+ stdx.zeroed(&filter.reserved);
1661
+
1662
+ if (!filter_valid) return null;
1663
+
1664
+ const transfers_groove: *TransfersGroove = &self.forest.grooves.transfers;
1665
+ const scan_builder: *TransfersGroove.ScanBuilder = &transfers_groove.scan_builder;
1666
+
1667
+ const timestamp_range: TimestampRange = .{
1668
+ .min = if (filter.timestamp_min == 0)
1669
+ TimestampRange.timestamp_min
1670
+ else
1671
+ filter.timestamp_min,
1672
+
1673
+ .max = if (filter.timestamp_max == 0)
1674
+ TimestampRange.timestamp_max
1675
+ else
1676
+ filter.timestamp_max,
1677
+ };
1678
+ assert(timestamp_range.min <= timestamp_range.max);
1679
+
1680
+ // This expression may have at most 5 scans, the `debit OR credit`
1681
+ // counts as just one:
1682
+ // ```
1683
+ // WHERE
1684
+ // (debit_account_id=? OR credit_account_id=?) AND
1685
+ // user_data_128=? AND
1686
+ // user_data_64=? AND
1687
+ // user_data_32=? AND
1688
+ // code=?
1689
+ // ```
1690
+ var scan_conditions: stdx.BoundedArrayType(*TransfersGroove.ScanBuilder.Scan, 5) = .{};
1691
+ const direction: Direction = if (filter.flags.reversed) .descending else .ascending;
1692
+
1693
+ // Adding the condition for `debit_account_id = $account_id`.
1694
+ if (filter.flags.debits) {
1695
+ scan_conditions.push(scan_builder.scan_prefix(
1696
+ .debit_account_id,
1697
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
1698
+ self.prefetch_snapshot.?,
1699
+ filter.account_id,
1700
+ timestamp_range,
1701
+ direction,
1702
+ ));
1703
+ }
1704
+
1705
+ // Adding the condition for `credit_account_id = $account_id`.
1706
+ if (filter.flags.credits) {
1707
+ scan_conditions.push(scan_builder.scan_prefix(
1708
+ .credit_account_id,
1709
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
1710
+ self.prefetch_snapshot.?,
1711
+ filter.account_id,
1712
+ timestamp_range,
1713
+ direction,
1714
+ ));
1715
+ }
1716
+
1717
+ switch (scan_conditions.count()) {
1718
+ 1 => {},
1719
+ 2 => {
1720
+ // Creating an union `OR` with the `debit_account_id` and `credit_account_id`.
1721
+ const accounts_merge = scan_builder.merge_union(scan_conditions.const_slice());
1722
+ scan_conditions.clear();
1723
+ scan_conditions.push(accounts_merge);
1724
+ },
1725
+ else => unreachable,
1726
+ }
1727
+
1728
+ // Additional filters with an intersection `AND`.
1729
+ inline for ([_]std.meta.FieldEnum(TransfersGroove.IndexTrees){
1730
+ .user_data_128, .user_data_64, .user_data_32, .code,
1731
+ }) |filter_field| {
1732
+ const filter_value = @field(filter, @tagName(filter_field));
1733
+ if (filter_value != 0) {
1734
+ scan_conditions.push(scan_builder.scan_prefix(
1735
+ filter_field,
1736
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
1737
+ self.prefetch_snapshot.?,
1738
+ filter_value,
1739
+ timestamp_range,
1740
+ direction,
1741
+ ));
1742
+ }
1743
+ }
1744
+
1745
+ return switch (scan_conditions.count()) {
1746
+ 1 => scan_conditions.get(0),
1747
+ 2...5 => scan_builder.merge_intersection(scan_conditions.const_slice()),
1748
+ else => unreachable,
1749
+ };
1750
+ }
1751
+
1752
+ fn prefetch_query_accounts(self: *StateMachine) void {
1753
+ assert(self.prefetch_input != null);
1754
+ assert(self.prefetch_operation.? == .query_accounts or
1755
+ self.prefetch_operation.? == .deprecated_query_accounts_unbatched);
1756
+ assert(self.scan_lookup == .null);
1757
+ assert(self.scan_lookup_buffer_index == 0);
1758
+ assert(self.scan_lookup_results.items.len == 0);
1759
+
1760
+ const filter: *const QueryFilter = self.get_prefetch_query_filter().?;
1761
+ self.prefetch_query_accounts_scan(filter);
1762
+ }
1763
+
1764
+ fn prefetch_query_accounts_scan(self: *StateMachine, filter: *const QueryFilter) void {
1765
+ assert(self.prefetch_input != null);
1766
+ assert(self.prefetch_operation.? == .query_accounts or
1767
+ self.prefetch_operation.? == .deprecated_query_accounts_unbatched);
1768
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1769
+ maybe(self.scan_lookup_results.items.len > 0);
1770
+
1771
+ log.debug("{?}: prefetch_query_accounts_scan: {}", .{
1772
+ self.forest.grid.superblock.replica_index,
1773
+ filter,
1774
+ });
1775
+
1776
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1777
+ if (self.get_scan_from_query_filter(
1778
+ AccountsGroove,
1779
+ &self.forest.grooves.accounts,
1780
+ filter,
1781
+ )) |scan| {
1782
+ assert(self.forest.scan_buffer_pool.scan_buffer_used > 0);
1783
+
1784
+ const scan_buffer = stdx.bytes_as_slice(
1785
+ .inexact,
1786
+ Account,
1787
+ self.scan_lookup_buffer[self.scan_lookup_buffer_index..],
1788
+ );
1789
+
1790
+ const scan_lookup = self.scan_lookup.get(.accounts);
1791
+ scan_lookup.* = AccountsScanLookup.init(
1792
+ &self.forest.grooves.accounts,
1793
+ scan,
1794
+ );
1795
+
1796
+ // Limiting the buffer size according to the query limit.
1797
+ // TODO: Prevent clients from setting the limit larger than the reply size by
1798
+ // failing with `TooMuchData`.
1799
+ const limit = @min(
1800
+ filter.limit,
1801
+ self.prefetch_operation.?.result_max(self.batch_size_limit),
1802
+ );
1803
+ assert(limit > 0);
1804
+ assert(scan_buffer.len >= limit);
1805
+ scan_lookup.read(
1806
+ scan_buffer[0..limit],
1807
+ &prefetch_query_accounts_scan_callback,
1808
+ );
1809
+ return;
1810
+ }
1811
+
1812
+ // TODO(batiati): Improve the way we do validations on the state machine.
1813
+ log.info("invalid filter for query_accounts: {any}", .{filter});
1814
+ self.forest.grid.on_next_tick(
1815
+ &prefetch_scan_next_tick_callback,
1816
+ &self.scan_lookup_next_tick,
1817
+ );
1818
+ }
1819
+
1820
+ fn prefetch_query_accounts_scan_callback(
1821
+ scan_lookup: *AccountsScanLookup,
1822
+ results: []const Account,
1823
+ ) void {
1824
+ const self: *StateMachine = ScanLookup.parent(.accounts, scan_lookup);
1825
+ assert(self.prefetch_input != null);
1826
+ assert(self.prefetch_operation.? == .query_accounts or
1827
+ self.prefetch_operation.? == .deprecated_query_accounts_unbatched);
1828
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1829
+ maybe(self.scan_lookup_results.items.len > 0);
1830
+
1831
+ self.scan_lookup_buffer_index += @intCast(results.len * @sizeOf(Account));
1832
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
1833
+
1834
+ self.scan_lookup = .null;
1835
+ self.forest.scan_buffer_pool.reset();
1836
+ self.forest.grooves.accounts.scan_builder.reset();
1837
+
1838
+ return self.prefetch_scan_resume();
1839
+ }
1840
+
1841
+ fn prefetch_query_transfers(self: *StateMachine) void {
1842
+ assert(self.prefetch_input != null);
1843
+ assert(self.prefetch_operation.? == .query_transfers or
1844
+ self.prefetch_operation.? == .deprecated_query_transfers_unbatched);
1845
+ assert(self.scan_lookup == .null);
1846
+ assert(self.scan_lookup_buffer_index == 0);
1847
+ assert(self.scan_lookup_results.items.len == 0);
1848
+
1849
+ const filter: *const QueryFilter = self.get_prefetch_query_filter().?;
1850
+ self.prefetch_query_transfers_scan(filter);
1851
+ }
1852
+
1853
+ fn prefetch_query_transfers_scan(self: *StateMachine, filter: *const QueryFilter) void {
1854
+ assert(self.prefetch_input != null);
1855
+ assert(self.prefetch_operation.? == .query_transfers or
1856
+ self.prefetch_operation.? == .deprecated_query_transfers_unbatched);
1857
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1858
+ maybe(self.scan_lookup_results.items.len > 0);
1859
+
1860
+ log.debug("{?}: prefetch_query_transfers_scan: {}", .{
1861
+ self.forest.grid.superblock.replica_index,
1862
+ filter,
1863
+ });
1864
+
1865
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1866
+ if (self.get_scan_from_query_filter(
1867
+ TransfersGroove,
1868
+ &self.forest.grooves.transfers,
1869
+ filter,
1870
+ )) |scan| {
1871
+ assert(self.forest.scan_buffer_pool.scan_buffer_used > 0);
1872
+
1873
+ const scan_buffer = stdx.bytes_as_slice(
1874
+ .inexact,
1875
+ Transfer,
1876
+ self.scan_lookup_buffer[self.scan_lookup_buffer_index..],
1877
+ );
1878
+
1879
+ const scan_lookup = self.scan_lookup.get(.transfers);
1880
+ scan_lookup.* = TransfersScanLookup.init(
1881
+ &self.forest.grooves.transfers,
1882
+ scan,
1883
+ );
1884
+
1885
+ // Limiting the buffer size according to the query limit.
1886
+ // TODO: Prevent clients from setting the limit larger than the buffer size.
1887
+ const limit = @min(
1888
+ filter.limit,
1889
+ self.prefetch_operation.?.result_max(self.batch_size_limit),
1890
+ );
1891
+ assert(limit > 0);
1892
+ assert(scan_buffer.len >= limit);
1893
+ scan_lookup.read(
1894
+ scan_buffer[0..limit],
1895
+ &prefetch_query_transfers_scan_callback,
1896
+ );
1897
+ return;
1898
+ }
1899
+
1900
+ // TODO(batiati): Improve the way we do validations on the state machine.
1901
+ log.info("invalid filter for query_transfers: {any}", .{filter});
1902
+ self.forest.grid.on_next_tick(
1903
+ &prefetch_scan_next_tick_callback,
1904
+ &self.scan_lookup_next_tick,
1905
+ );
1906
+ }
1907
+
1908
+ fn prefetch_query_transfers_scan_callback(
1909
+ scan_lookup: *TransfersScanLookup,
1910
+ results: []const Transfer,
1911
+ ) void {
1912
+ const self: *StateMachine = ScanLookup.parent(.transfers, scan_lookup);
1913
+ assert(self.prefetch_input != null);
1914
+ assert(self.prefetch_operation.? == .query_transfers or
1915
+ self.prefetch_operation.? == .deprecated_query_transfers_unbatched);
1916
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
1917
+ maybe(self.scan_lookup_results.items.len > 0);
1918
+
1919
+ self.scan_lookup_buffer_index += @intCast(results.len * @sizeOf(Transfer));
1920
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
1921
+
1922
+ self.scan_lookup = .null;
1923
+ self.forest.scan_buffer_pool.reset();
1924
+ self.forest.grooves.transfers.scan_builder.reset();
1925
+
1926
+ return self.prefetch_scan_resume();
1927
+ }
1928
+
1929
+ /// Returns the `QueryFilter` from the prefetch input buffer.
1930
+ /// In the case of multi-batch inputs, returns the current filter
1931
+ /// or `null` if all filters have been executed.
1932
+ fn get_prefetch_query_filter(self: *StateMachine) ?*const QueryFilter {
1933
+ assert(self.prefetch_input != null);
1934
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
1935
+
1936
+ switch (self.prefetch_operation.?) {
1937
+ .query_accounts, .query_transfers => {
1938
+ const filter_index = self.scan_lookup_results.items.len;
1939
+ maybe(filter_index > 0);
1940
+
1941
+ const filters = stdx.bytes_as_slice(
1942
+ .exact,
1943
+ QueryFilter,
1944
+ self.prefetch_input.?,
1945
+ );
1946
+ assert(filters.len > 0);
1947
+ assert(filter_index <= filters.len);
1948
+
1949
+ // Returns null if all filters were processed.
1950
+ if (filter_index == filters.len) return null;
1951
+ return &filters[filter_index];
1952
+ },
1953
+ .deprecated_query_accounts_unbatched, .deprecated_query_transfers_unbatched => {
1954
+ // Operations not encoded as multi-batch must have only a single filter.
1955
+ assert(self.scan_lookup_results.items.len == 0);
1956
+ const filter: *const QueryFilter = @alignCast(std.mem.bytesAsValue(
1957
+ QueryFilter,
1958
+ self.prefetch_input.?,
1959
+ ));
1960
+ return filter;
1961
+ },
1962
+ else => unreachable,
1963
+ }
1964
+ }
1965
+
1966
+ fn get_scan_from_query_filter(
1967
+ self: *StateMachine,
1968
+ comptime Groove: type,
1969
+ groove: *Groove,
1970
+ filter: *const QueryFilter,
1971
+ ) ?*Groove.ScanBuilder.Scan {
1972
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
1973
+
1974
+ const filter_valid =
1975
+ (filter.timestamp_min == 0 or TimestampRange.valid(filter.timestamp_min)) and
1976
+ (filter.timestamp_max == 0 or TimestampRange.valid(filter.timestamp_max)) and
1977
+ (filter.timestamp_max == 0 or filter.timestamp_min <= filter.timestamp_max) and
1978
+ filter.limit != 0 and
1979
+ filter.flags.padding == 0 and
1980
+ stdx.zeroed(&filter.reserved);
1981
+
1982
+ if (!filter_valid) return null;
1983
+
1984
+ const direction: Direction = if (filter.flags.reversed) .descending else .ascending;
1985
+ const timestamp_range: TimestampRange = .{
1986
+ .min = if (filter.timestamp_min == 0)
1987
+ TimestampRange.timestamp_min
1988
+ else
1989
+ filter.timestamp_min,
1990
+
1991
+ .max = if (filter.timestamp_max == 0)
1992
+ TimestampRange.timestamp_max
1993
+ else
1994
+ filter.timestamp_max,
1995
+ };
1996
+ assert(timestamp_range.min <= timestamp_range.max);
1997
+
1998
+ const indexes = [_]std.meta.FieldEnum(QueryFilter){
1999
+ .user_data_128,
2000
+ .user_data_64,
2001
+ .user_data_32,
2002
+ .ledger,
2003
+ .code,
2004
+ };
2005
+ comptime assert(indexes.len <= constants.lsm_scans_max);
2006
+
2007
+ var scan_conditions: stdx.BoundedArrayType(*Groove.ScanBuilder.Scan, indexes.len) = .{};
2008
+ inline for (indexes) |index| {
2009
+ if (@field(filter, @tagName(index)) != 0) {
2010
+ scan_conditions.push(groove.scan_builder.scan_prefix(
2011
+ std.enums.nameCast(std.meta.FieldEnum(Groove.IndexTrees), index),
2012
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
2013
+ self.prefetch_snapshot.?,
2014
+ @field(filter, @tagName(index)),
2015
+ timestamp_range,
2016
+ direction,
2017
+ ));
2018
+ }
2019
+ }
2020
+
2021
+ return switch (scan_conditions.count()) {
2022
+ 0 =>
2023
+ // TODO(batiati): Querying only by timestamp uses the Object groove,
2024
+ // we could skip the lookup step entirely then.
2025
+ // It will be implemented as part of the query executor.
2026
+ groove.scan_builder.scan_timestamp(
2027
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
2028
+ self.prefetch_snapshot.?,
2029
+ timestamp_range,
2030
+ direction,
2031
+ ),
2032
+ 1 => scan_conditions.get(0),
2033
+ else => groove.scan_builder.merge_intersection(scan_conditions.const_slice()),
2034
+ };
2035
+ }
2036
+
2037
+ /// Common `next_tick` callback used by all `prefetch_scan_*` functions to complete
2038
+ /// the operation when the filter is invalid.
2039
+ fn prefetch_scan_next_tick_callback(completion: *Grid.NextTick) void {
2040
+ const self: *StateMachine = @alignCast(@fieldParentPtr(
2041
+ "scan_lookup_next_tick",
2042
+ completion,
2043
+ ));
2044
+
2045
+ // Invalid filter, no results found.
2046
+ self.scan_lookup_results.appendAssumeCapacity(0);
2047
+
2048
+ self.prefetch_scan_resume();
2049
+ }
2050
+
2051
+ fn prefetch_scan_resume(self: *StateMachine) void {
2052
+ assert(self.prefetch_input != null);
2053
+ assert(self.prefetch_operation != null);
2054
+ assert(self.scan_lookup == .null);
2055
+ maybe(self.scan_lookup_buffer_index > 0);
2056
+ maybe(self.scan_lookup_results.items.len > 0);
2057
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
2058
+
2059
+ // Processes the next filter in the case of multi-batch messages.
2060
+ switch (self.prefetch_operation.?) {
2061
+ .get_account_transfers => {
2062
+ if (self.get_prefetch_account_filter()) |filter_next| {
2063
+ return self.prefetch_get_account_transfers_scan(filter_next);
2064
+ }
2065
+ },
2066
+ .get_account_balances => {
2067
+ if (self.get_prefetch_account_filter()) |filter_next| {
2068
+ return self.prefetch_get_account_balances_scan(filter_next);
2069
+ }
2070
+ },
2071
+ .query_accounts => {
2072
+ if (self.get_prefetch_query_filter()) |filter_next| {
2073
+ return self.prefetch_query_accounts_scan(filter_next);
2074
+ }
2075
+ },
2076
+ .query_transfers => {
2077
+ if (self.get_prefetch_query_filter()) |filter_next| {
2078
+ return self.prefetch_query_transfers_scan(filter_next);
2079
+ }
2080
+ },
2081
+ .get_change_events => {},
2082
+
2083
+ .deprecated_get_account_transfers_unbatched,
2084
+ .deprecated_get_account_balances_unbatched,
2085
+ .deprecated_query_accounts_unbatched,
2086
+ .deprecated_query_transfers_unbatched,
2087
+ => {},
2088
+
2089
+ else => unreachable, // Not query operations.
2090
+ }
2091
+
2092
+ self.prefetch_finish();
2093
+ }
2094
+
2095
+ fn prefetch_get_change_events(self: *StateMachine) void {
2096
+ assert(self.prefetch_input != null);
2097
+ assert(self.prefetch_operation.? == .get_change_events);
2098
+ assert(self.scan_lookup == .null);
2099
+ assert(self.scan_lookup_buffer_index == 0);
2100
+ assert(self.scan_lookup_results.items.len == 0);
2101
+
2102
+ const filter: *const ChangeEventsFilter = self.get_prefetch_event_filter();
2103
+ self.prefetch_get_change_events_scan(filter);
2104
+ }
2105
+
2106
+ fn prefetch_get_change_events_scan(
2107
+ self: *StateMachine,
2108
+ filter: *const ChangeEventsFilter,
2109
+ ) void {
2110
+ assert(self.prefetch_input != null);
2111
+ assert(self.prefetch_operation.? == .get_change_events);
2112
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
2113
+ maybe(self.scan_lookup_results.items.len > 0);
2114
+
2115
+ log.debug("{?}: prefetch_get_change_events_scan: {}", .{
2116
+ self.forest.grid.superblock.replica_index,
2117
+ filter,
2118
+ });
2119
+
2120
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
2121
+ if (self.get_scan_from_change_events_filter(filter)) |scan_lookup| {
2122
+ assert(self.forest.scan_buffer_pool.scan_buffer_used > 0);
2123
+
2124
+ const scan_buffer = stdx.bytes_as_slice(
2125
+ .inexact,
2126
+ AccountEvent,
2127
+ self.scan_lookup_buffer[self.scan_lookup_buffer_index..],
2128
+ );
2129
+
2130
+ // TODO: For queries, the number of available prefetches may need to be considered:
2131
+ // - In cases like `query_accounts` and `query_transfers`, no prefetching is
2132
+ // required, so the limit is simply `message_body_size_max / result_size`.
2133
+ // - In `get_account_balances`, one object is prefetched per each event (the query
2134
+ // filter).
2135
+ // - In `get_change_events` and `expire_pending_transfers`, objects are prefetched
2136
+ // per scanned result.
2137
+ // We could either:
2138
+ // - Calculate the number of prefetches based on the event/reply size, as is done
2139
+ // for `create_*` and `lookup_*` operations;
2140
+ // - Or, make the `operation_{event,result}_max(...)` functions aware of the number
2141
+ // of prefetches.
2142
+ const limit_max: u32 = limit_max: {
2143
+ const result_max = self.prefetch_operation.?.result_max(self.batch_size_limit);
2144
+ // Also constrained by the maximum number of available prefetches.
2145
+ const prefetch_transfers = @max(
2146
+ Operation.lookup_transfers.event_max(self.batch_size_limit),
2147
+ Operation.deprecated_lookup_transfers_unbatched.event_max(
2148
+ self.batch_size_limit,
2149
+ ),
2150
+ );
2151
+ const prefetch_accounts = @max(
2152
+ Operation.lookup_accounts.event_max(self.batch_size_limit),
2153
+ Operation.deprecated_lookup_accounts_unbatched.event_max(
2154
+ self.batch_size_limit,
2155
+ ),
2156
+ );
2157
+
2158
+ break :limit_max @min(
2159
+ result_max,
2160
+ prefetch_transfers,
2161
+ // Each event == 2 accounts.
2162
+ @divFloor(prefetch_accounts, 2),
2163
+ );
2164
+ };
2165
+
2166
+ // Limiting the buffer size according to the query limit.
2167
+ // TODO: Prevent clients from setting the limit larger than the buffer size.
2168
+ const limit = @min(filter.limit, limit_max);
2169
+ assert(limit > 0);
2170
+ assert(scan_buffer.len >= limit);
2171
+ scan_lookup.read(
2172
+ scan_buffer[0..limit],
2173
+ &prefetch_get_change_events_scan_callback,
2174
+ );
2175
+ return;
2176
+ }
2177
+
2178
+ // TODO(batiati): Improve the way we do validations on the state machine.
2179
+ log.info("invalid filter for prefetch_get_change_events_scan: {any}", .{filter});
2180
+ self.forest.grid.on_next_tick(
2181
+ &prefetch_scan_next_tick_callback,
2182
+ &self.scan_lookup_next_tick,
2183
+ );
2184
+ }
2185
+
2186
+ fn prefetch_get_change_events_scan_callback(
2187
+ scan_lookup: *ChangeEventsScanLookup,
2188
+ results: []const AccountEvent,
2189
+ ) void {
2190
+ const self: *StateMachine = ScanLookup.parent(.change_events, scan_lookup);
2191
+ assert(self.prefetch_input != null);
2192
+ assert(self.prefetch_operation.? == .get_change_events);
2193
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
2194
+ assert(self.scan_lookup_results.items.len == 0);
2195
+
2196
+ self.scan_lookup_buffer_index += @intCast(results.len * @sizeOf(AccountEvent));
2197
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
2198
+
2199
+ self.forest.scan_buffer_pool.reset();
2200
+ self.forest.grooves.account_events.scan_builder.reset();
2201
+ self.scan_lookup = .null;
2202
+
2203
+ if (results.len == 0) return self.prefetch_finish();
2204
+
2205
+ const accounts: *AccountsGroove = &self.forest.grooves.accounts;
2206
+ const transfers: *TransfersGroove = &self.forest.grooves.transfers;
2207
+ for (results) |result| {
2208
+ switch (result.schema()) {
2209
+ .current => {
2210
+ assert(result.dr_account_timestamp != 0);
2211
+ assert(result.cr_account_timestamp != 0);
2212
+
2213
+ accounts.prefetch_enqueue_by_timestamp(result.dr_account_timestamp);
2214
+ accounts.prefetch_enqueue_by_timestamp(result.cr_account_timestamp);
2215
+ if (result.transfer_pending_status == .expired) {
2216
+ // For expiry events, the timestamp isn't associated with any transfer.
2217
+ // Instead, the original pending transfer is prefetched.
2218
+ assert(result.transfer_pending_id != 0);
2219
+ transfers.prefetch_enqueue(result.transfer_pending_id);
2220
+ } else {
2221
+ transfers.prefetch_enqueue_by_timestamp(result.timestamp);
2222
+ }
2223
+ },
2224
+ .former => |former| {
2225
+ // In the former schema:
2226
+ // If either the debit or credit account ID is zero (one side without
2227
+ // the history flag), the lookup would have already omitted the event
2228
+ // from the results.
2229
+ assert(former.dr_account_id != 0);
2230
+ assert(former.cr_account_id != 0);
2231
+
2232
+ accounts.prefetch_enqueue(former.dr_account_id);
2233
+ accounts.prefetch_enqueue(former.cr_account_id);
2234
+ transfers.prefetch_enqueue_by_timestamp(former.timestamp);
2235
+ },
2236
+ }
2237
+ }
2238
+
2239
+ accounts.prefetch(
2240
+ prefetch_get_change_events_callback_accounts,
2241
+ self.prefetch_context.get(.accounts),
2242
+ );
2243
+ }
2244
+
2245
+ fn prefetch_get_change_events_callback_accounts(
2246
+ completion: *AccountsGroove.PrefetchContext,
2247
+ ) void {
2248
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
2249
+ assert(self.prefetch_input != null);
2250
+ assert(self.prefetch_operation.? == .get_change_events);
2251
+ self.prefetch_context = .null;
2252
+
2253
+ self.forest.grooves.transfers.prefetch(
2254
+ prefetch_get_change_events_callback_transfers,
2255
+ self.prefetch_context.get(.transfers),
2256
+ );
2257
+ }
2258
+
2259
+ fn prefetch_get_change_events_callback_transfers(
2260
+ completion: *TransfersGroove.PrefetchContext,
2261
+ ) void {
2262
+ const self: *StateMachine = PrefetchContext.parent(.transfers, completion);
2263
+ assert(self.prefetch_input != null);
2264
+ assert(self.prefetch_operation.? == .get_change_events);
2265
+
2266
+ self.prefetch_context = .null;
2267
+ self.prefetch_finish();
2268
+ }
2269
+
2270
+ /// Returns the `EventFilter` from the prefetch input buffer.
2271
+ fn get_prefetch_event_filter(self: *StateMachine) *const ChangeEventsFilter {
2272
+ assert(self.prefetch_input != null);
2273
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
2274
+
2275
+ // Operations not encoded as multi-batch must have only a single filter.
2276
+ assert(self.scan_lookup_results.items.len == 0);
2277
+ const filter: *const ChangeEventsFilter = @alignCast(std.mem.bytesAsValue(
2278
+ ChangeEventsFilter,
2279
+ self.prefetch_input.?,
2280
+ ));
2281
+ return filter;
2282
+ }
2283
+
2284
+ fn get_scan_from_change_events_filter(
2285
+ self: *StateMachine,
2286
+ filter: *const ChangeEventsFilter,
2287
+ ) ?*ChangeEventsScanLookup {
2288
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
2289
+
2290
+ const filter_valid =
2291
+ (filter.timestamp_min == 0 or TimestampRange.valid(filter.timestamp_min)) and
2292
+ (filter.timestamp_max == 0 or TimestampRange.valid(filter.timestamp_max)) and
2293
+ (filter.timestamp_max == 0 or filter.timestamp_min <= filter.timestamp_max) and
2294
+ filter.limit != 0 and
2295
+ stdx.zeroed(&filter.reserved);
2296
+
2297
+ if (!filter_valid) return null;
2298
+
2299
+ // CDC is always in ascending order.
2300
+ const timestamp_range: TimestampRange = .{
2301
+ .min = if (filter.timestamp_min == 0)
2302
+ TimestampRange.timestamp_min
2303
+ else
2304
+ filter.timestamp_min,
2305
+
2306
+ .max = if (filter.timestamp_max == 0)
2307
+ TimestampRange.timestamp_max
2308
+ else
2309
+ filter.timestamp_max,
2310
+ };
2311
+ assert(timestamp_range.min <= timestamp_range.max);
2312
+
2313
+ const scan_lookup: *ChangeEventsScanLookup = self.scan_lookup.get(.change_events);
2314
+ scan_lookup.init(
2315
+ &self.forest.grooves.account_events.objects,
2316
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
2317
+ self.prefetch_snapshot.?,
2318
+ timestamp_range,
2319
+ );
2320
+
2321
+ return scan_lookup;
2322
+ }
2323
+
2324
+ fn prefetch_expire_pending_transfers(self: *StateMachine) void {
2325
+ assert(self.prefetch_input != null);
2326
+ assert(self.prefetch_operation.? == .pulse);
2327
+ assert(self.scan_lookup_buffer_index == 0);
2328
+ assert(self.scan_lookup_results.items.len == 0);
2329
+ assert(self.forest.scan_buffer_pool.scan_buffer_used == 0);
2330
+ assert(TimestampRange.valid(self.prefetch_timestamp));
2331
+
2332
+ // We must be constrained to the same limit as `create_transfers`.
2333
+ const scan_buffer_size = @max(
2334
+ Operation.create_transfers.event_max(self.batch_size_limit),
2335
+ Operation.deprecated_create_transfers_unbatched.event_max(self.batch_size_limit),
2336
+ ) * @sizeOf(Transfer);
2337
+
2338
+ const scan_lookup_buffer = stdx.bytes_as_slice(
2339
+ .inexact,
2340
+ Transfer,
2341
+ self.scan_lookup_buffer[0..scan_buffer_size],
2342
+ );
2343
+
2344
+ const transfers_groove: *TransfersGroove = &self.forest.grooves.transfers;
2345
+ const scan = self.expire_pending_transfers.scan(
2346
+ &transfers_groove.indexes.expires_at,
2347
+ self.forest.scan_buffer_pool.acquire_assume_capacity(),
2348
+ .{
2349
+ .snapshot = transfers_groove.prefetch_snapshot.?,
2350
+ .expires_at_max = self.prefetch_timestamp,
2351
+ },
2352
+ );
2353
+
2354
+ const scan_lookup = self.scan_lookup.get(.expire_pending_transfers);
2355
+ scan_lookup.* = ExpirePendingTransfers.ScanLookup.init(
2356
+ transfers_groove,
2357
+ scan,
2358
+ );
2359
+ scan_lookup.read(
2360
+ scan_lookup_buffer,
2361
+ &prefetch_expire_pending_transfers_scan_callback,
2362
+ );
2363
+ }
2364
+
2365
+ fn prefetch_expire_pending_transfers_scan_callback(
2366
+ scan_lookup: *ExpirePendingTransfers.ScanLookup,
2367
+ results: []const Transfer,
2368
+ ) void {
2369
+ const self: *StateMachine = ScanLookup.parent(.expire_pending_transfers, scan_lookup);
2370
+ assert(self.prefetch_input != null);
2371
+ assert(self.prefetch_operation.? == .pulse);
2372
+ assert(self.scan_lookup_buffer_index < self.scan_lookup_buffer.len);
2373
+ assert(self.scan_lookup_results.items.len == 0);
2374
+
2375
+ self.expire_pending_transfers.finish(scan_lookup.state, results);
2376
+ self.scan_lookup_buffer_index = @intCast(results.len * @sizeOf(Transfer));
2377
+ self.scan_lookup_results.appendAssumeCapacity(@intCast(results.len));
2378
+
2379
+ self.scan_lookup = .null;
2380
+ self.forest.scan_buffer_pool.reset();
2381
+ self.forest.grooves.transfers.scan_builder.reset();
2382
+
2383
+ self.prefetch_expire_pending_transfers_accounts();
2384
+ }
2385
+
2386
+ fn prefetch_expire_pending_transfers_accounts(self: *StateMachine) void {
2387
+ assert(self.prefetch_input != null);
2388
+ assert(self.prefetch_operation.? == .pulse);
2389
+ assert(self.scan_lookup_results.items.len == 1);
2390
+ maybe(self.scan_lookup_buffer_index == 0);
2391
+
2392
+ const result_count: u32 = self.scan_lookup_results.items[0];
2393
+ if (result_count == 0) return self.prefetch_finish();
2394
+
2395
+ const result_max: u32 = @max(
2396
+ Operation.create_transfers.event_max(self.batch_size_limit),
2397
+ Operation.deprecated_create_transfers_unbatched.event_max(self.batch_size_limit),
2398
+ );
2399
+ assert(result_count <= result_max);
2400
+ assert(self.scan_lookup_buffer_index == result_count * @sizeOf(Transfer));
2401
+ const transfers = stdx.bytes_as_slice(
2402
+ .exact,
2403
+ Transfer,
2404
+ self.scan_lookup_buffer[0..self.scan_lookup_buffer_index],
2405
+ );
2406
+
2407
+ const grooves = &self.forest.grooves;
2408
+ for (transfers) |expired| {
2409
+ assert(expired.flags.pending == true);
2410
+ const expires_at = expired.timestamp + expired.timeout_ns();
2411
+
2412
+ assert(expires_at <= self.prefetch_timestamp);
2413
+
2414
+ grooves.accounts.prefetch_enqueue(expired.debit_account_id);
2415
+ grooves.accounts.prefetch_enqueue(expired.credit_account_id);
2416
+ grooves.transfers_pending.prefetch_enqueue(expired.timestamp);
2417
+ }
2418
+
2419
+ self.forest.grooves.accounts.prefetch(
2420
+ prefetch_expire_pending_transfers_callback_accounts,
2421
+ self.prefetch_context.get(.accounts),
2422
+ );
2423
+ }
2424
+
2425
+ fn prefetch_expire_pending_transfers_callback_accounts(
2426
+ completion: *AccountsGroove.PrefetchContext,
2427
+ ) void {
2428
+ const self: *StateMachine = PrefetchContext.parent(.accounts, completion);
2429
+ assert(self.prefetch_input != null);
2430
+ assert(self.prefetch_operation.? == .pulse);
2431
+ self.prefetch_context = .null;
2432
+
2433
+ self.forest.grooves.transfers_pending.prefetch(
2434
+ prefetch_expire_pending_transfers_callback_transfers_pending,
2435
+ self.prefetch_context.get(.transfers_pending),
2436
+ );
2437
+ }
2438
+
2439
+ fn prefetch_expire_pending_transfers_callback_transfers_pending(
2440
+ completion: *TransfersPendingGroove.PrefetchContext,
2441
+ ) void {
2442
+ const self: *StateMachine = PrefetchContext.parent(.transfers_pending, completion);
2443
+ assert(self.prefetch_input != null);
2444
+ assert(self.prefetch_operation.? == .pulse);
2445
+
2446
+ self.prefetch_context = .null;
2447
+ self.prefetch_finish();
2448
+ }
2449
+
2450
+ pub fn commit(
2451
+ self: *StateMachine,
2452
+ client: u128,
2453
+ op: u64,
2454
+ timestamp: u64,
2455
+ operation: Operation,
2456
+ message_body_used: []align(16) const u8,
2457
+ output_buffer: *align(16) [constants.message_body_size_max]u8,
2458
+ ) usize {
2459
+ // NB: This function should never accept `client_release` as an argument.
2460
+ // Any public API changes must be introduced explicitly as a new `operation` number.
2461
+ assert(op != 0);
2462
+ assert(timestamp > self.commit_timestamp or self.aof_recovery);
2463
+ assert(message_body_used.len <= self.batch_size_limit);
2464
+ if (client == 0) assert(operation == .pulse);
2465
+
2466
+ maybe(self.scan_lookup_buffer_index > 0);
2467
+ maybe(self.scan_lookup_results.items.len > 0);
2468
+ defer {
2469
+ assert(self.scan_lookup_buffer_index == 0);
2470
+ assert(self.scan_lookup_results.items.len == 0);
2471
+ }
2472
+
2473
+ const result: usize = switch (operation) {
2474
+ .pulse => self.execute_expire_pending_transfers(timestamp),
2475
+ inline .create_accounts,
2476
+ .create_transfers,
2477
+ .lookup_accounts,
2478
+ .lookup_transfers,
2479
+ => |operation_comptime| self.execute_multi_batch(
2480
+ timestamp,
2481
+ operation_comptime,
2482
+ message_body_used,
2483
+ output_buffer,
2484
+ ),
2485
+ inline .get_account_transfers,
2486
+ .get_account_balances,
2487
+ .query_accounts,
2488
+ .query_transfers,
2489
+ => |operation_comptime| self.execute_query_multi_batch(
2490
+ operation_comptime,
2491
+ message_body_used,
2492
+ output_buffer,
2493
+ ),
2494
+ .get_change_events => self.execute_query(
2495
+ .get_change_events,
2496
+ message_body_used,
2497
+ output_buffer,
2498
+ ),
2499
+
2500
+ inline .deprecated_create_accounts_unbatched,
2501
+ .deprecated_create_transfers_unbatched,
2502
+ .deprecated_lookup_accounts_unbatched,
2503
+ .deprecated_lookup_transfers_unbatched,
2504
+ => |operation_comptime| self.execute(
2505
+ timestamp,
2506
+ operation_comptime,
2507
+ message_body_used,
2508
+ output_buffer,
2509
+ ),
2510
+ inline .deprecated_get_account_transfers_unbatched,
2511
+ .deprecated_get_account_balances_unbatched,
2512
+ .deprecated_query_accounts_unbatched,
2513
+ .deprecated_query_transfers_unbatched,
2514
+ => |operation_comptime| self.execute_query(
2515
+ operation_comptime,
2516
+ message_body_used,
2517
+ output_buffer,
2518
+ ),
2519
+ };
2520
+
2521
+ @setEvalBranchQuota(10_000);
2522
+ switch (operation) {
2523
+ .pulse => {},
2524
+ inline else => |operation_comptime| {
2525
+ const event_size: u32 = operation_comptime.event_size();
2526
+ const batch_count: u32 = batch_count: {
2527
+ if (!operation_comptime.is_multi_batch()) {
2528
+ break :batch_count @intCast(@divExact(
2529
+ message_body_used.len,
2530
+ event_size,
2531
+ ));
2532
+ }
2533
+ comptime assert(operation_comptime.is_multi_batch());
2534
+
2535
+ const body_decoder = MultiBatchDecoder.init(message_body_used, .{
2536
+ .element_size = event_size,
2537
+ }) catch unreachable; // Already validated by `input_valid()`.
2538
+ break :batch_count @intCast(@divExact(
2539
+ body_decoder.payload.len,
2540
+ event_size,
2541
+ ));
2542
+ };
2543
+ const duration = self.metrics.timer.read();
2544
+
2545
+ self.metrics.record(
2546
+ Metrics.from_operation(operation_comptime),
2547
+ duration.to_us(),
2548
+ batch_count,
2549
+ );
2550
+ },
2551
+ }
2552
+ return result;
2553
+ }
2554
+
2555
+ fn execute(
2556
+ self: *StateMachine,
2557
+ timestamp: u64,
2558
+ comptime operation: Operation,
2559
+ message_body_used: []align(16) const u8,
2560
+ output_buffer: *align(16) [constants.message_body_size_max]u8,
2561
+ ) usize {
2562
+ comptime assert(!operation.is_multi_batch());
2563
+ comptime assert(operation.is_batchable());
2564
+
2565
+ switch (operation) {
2566
+ .deprecated_create_accounts_unbatched,
2567
+ .deprecated_create_transfers_unbatched,
2568
+ => return self.execute_create(
2569
+ operation,
2570
+ timestamp,
2571
+ message_body_used,
2572
+ output_buffer,
2573
+ ),
2574
+ .deprecated_lookup_accounts_unbatched => return self.execute_lookup_accounts(
2575
+ message_body_used,
2576
+ output_buffer,
2577
+ ),
2578
+ .deprecated_lookup_transfers_unbatched => return self.execute_lookup_transfers(
2579
+ message_body_used,
2580
+ output_buffer,
2581
+ ),
2582
+ else => comptime unreachable,
2583
+ }
2584
+ }
2585
+
2586
+ fn execute_multi_batch(
2587
+ self: *StateMachine,
2588
+ timestamp: u64,
2589
+ comptime operation: Operation,
2590
+ message_body_used: []align(16) const u8,
2591
+ output_buffer: *align(16) [constants.message_body_size_max]u8,
2592
+ ) usize {
2593
+ comptime assert(operation.is_multi_batch());
2594
+ comptime assert(operation.is_batchable());
2595
+
2596
+ var body_decoder = MultiBatchDecoder.init(message_body_used, .{
2597
+ .element_size = operation.event_size(),
2598
+ }) catch unreachable; // Already validated by `input_valid()`.
2599
+ assert(body_decoder.batch_count() >= 1);
2600
+ var reply_encoder = MultiBatchEncoder.init(output_buffer, .{
2601
+ .element_size = operation.result_size(),
2602
+ });
2603
+
2604
+ var execute_timestamp: u64 = timestamp -
2605
+ self.prepare_delta_nanoseconds(
2606
+ operation,
2607
+ body_decoder.payload, // The entire message's body without the trailer.
2608
+ );
2609
+ while (body_decoder.pop()) |batch| {
2610
+ assert(self.batch_valid(operation, batch));
2611
+ // Commit each batched set of events
2612
+ // using the timestamp of the highest result of the response.
2613
+ execute_timestamp += self.prepare_delta_nanoseconds(
2614
+ operation,
2615
+ batch, // The batch's body.
2616
+ );
2617
+ const bytes_written: usize = switch (operation) {
2618
+ .create_accounts,
2619
+ .create_transfers,
2620
+ => self.execute_create(
2621
+ operation,
2622
+ execute_timestamp,
2623
+ batch,
2624
+ reply_encoder.writable().?,
2625
+ ),
2626
+ .lookup_accounts => self.execute_lookup_accounts(
2627
+ batch,
2628
+ reply_encoder.writable().?,
2629
+ ),
2630
+ .lookup_transfers => self.execute_lookup_transfers(
2631
+ batch,
2632
+ reply_encoder.writable().?,
2633
+ ),
2634
+ else => comptime unreachable,
2635
+ };
2636
+ reply_encoder.add(@intCast(bytes_written));
2637
+ }
2638
+ assert(execute_timestamp == timestamp);
2639
+ assert(body_decoder.batch_count() == reply_encoder.batch_count);
2640
+
2641
+ const encoded_bytes: usize = reply_encoder.finish();
2642
+ assert(encoded_bytes > 0);
2643
+ return encoded_bytes;
2644
+ }
2645
+
2646
+ fn execute_query(
2647
+ self: *StateMachine,
2648
+ comptime operation: Operation,
2649
+ message_body_used: []align(16) const u8,
2650
+ output_buffer: *align(16) [constants.message_body_size_max]u8,
2651
+ ) usize {
2652
+ comptime assert(!operation.is_multi_batch());
2653
+ comptime assert(!operation.is_batchable());
2654
+ assert(self.scan_lookup_results.items.len > 0);
2655
+ maybe(self.scan_lookup_buffer_index == 0);
2656
+ defer {
2657
+ self.scan_lookup_buffer_index = 0;
2658
+ self.scan_lookup_results.clearRetainingCapacity();
2659
+ }
2660
+
2661
+ assert(self.scan_lookup_results.items.len == 1);
2662
+ const result_count: u32 = self.scan_lookup_results.items[0];
2663
+ const result_size: u32 = self.scan_lookup_buffer_index;
2664
+ // Invalid filter or no results found.
2665
+ if (result_size == 0) {
2666
+ assert(result_count == 0);
2667
+ return 0;
2668
+ }
2669
+
2670
+ assert(result_count > 0);
2671
+ assert(result_size % result_count == 0);
2672
+ assert(result_size <= self.scan_lookup_buffer.len);
2673
+ const bytes_written: usize = switch (operation) {
2674
+ .get_change_events => self.execute_get_change_events(
2675
+ message_body_used,
2676
+ self.scan_lookup_buffer[0..result_size],
2677
+ output_buffer,
2678
+ ),
2679
+ .deprecated_get_account_transfers_unbatched => self.execute_get_account_transfers(
2680
+ message_body_used,
2681
+ self.scan_lookup_buffer[0..result_size],
2682
+ output_buffer,
2683
+ ),
2684
+ .deprecated_get_account_balances_unbatched => self.execute_get_account_balances(
2685
+ message_body_used,
2686
+ self.scan_lookup_buffer[0..result_size],
2687
+ output_buffer,
2688
+ ),
2689
+ .deprecated_query_transfers_unbatched => self.execute_query_transfers(
2690
+ message_body_used,
2691
+ self.scan_lookup_buffer[0..result_size],
2692
+ output_buffer,
2693
+ ),
2694
+ .deprecated_query_accounts_unbatched => self.execute_query_accounts(
2695
+ message_body_used,
2696
+ self.scan_lookup_buffer[0..result_size],
2697
+ output_buffer,
2698
+ ),
2699
+ else => comptime unreachable,
2700
+ };
2701
+ maybe(bytes_written == 0);
2702
+ return bytes_written;
2703
+ }
2704
+
2705
+ fn execute_query_multi_batch(
2706
+ self: *StateMachine,
2707
+ comptime operation: Operation,
2708
+ message_body_used: []align(16) const u8,
2709
+ output_buffer: *align(16) [constants.message_body_size_max]u8,
2710
+ ) usize {
2711
+ comptime assert(operation.is_multi_batch());
2712
+ comptime assert(!operation.is_batchable());
2713
+ assert(self.scan_lookup_results.items.len > 0);
2714
+ maybe(self.scan_lookup_buffer_index == 0);
2715
+ defer {
2716
+ self.scan_lookup_buffer_index = 0;
2717
+ self.scan_lookup_results.clearRetainingCapacity();
2718
+ }
2719
+
2720
+ var offset: u32 = 0;
2721
+ var body_decoder = MultiBatchDecoder.init(message_body_used, .{
2722
+ .element_size = operation.event_size(),
2723
+ }) catch unreachable; // Already validated by `input_valid()`.
2724
+ assert(body_decoder.batch_count() == self.scan_lookup_results.items.len);
2725
+ var reply_encoder = MultiBatchEncoder.init(output_buffer, .{
2726
+ .element_size = operation.result_size(),
2727
+ });
2728
+ for (self.scan_lookup_results.items) |result_count| {
2729
+ const batch: []const u8 = body_decoder.pop().?;
2730
+ const encoder_output_buffer: []u8 = reply_encoder.writable().?;
2731
+ const bytes_written: usize = switch (operation) {
2732
+ .get_account_transfers => size: {
2733
+ const scan_size: u32 = result_count * @sizeOf(Transfer);
2734
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
2735
+ assert(self.scan_lookup_buffer_index >= scan_size + offset);
2736
+ defer offset += scan_size;
2737
+
2738
+ break :size self.execute_get_account_transfers(
2739
+ batch,
2740
+ self.scan_lookup_buffer[offset..][0..scan_size],
2741
+ encoder_output_buffer,
2742
+ );
2743
+ },
2744
+ .get_account_balances => size: {
2745
+ const scan_size: u32 = result_count * @sizeOf(AccountEvent);
2746
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
2747
+ assert(self.scan_lookup_buffer_index >= scan_size + offset);
2748
+ defer offset += scan_size;
2749
+
2750
+ break :size self.execute_get_account_balances(
2751
+ batch,
2752
+ self.scan_lookup_buffer[offset..][0..scan_size],
2753
+ encoder_output_buffer,
2754
+ );
2755
+ },
2756
+ .query_transfers => size: {
2757
+ const scan_size: u32 = result_count * @sizeOf(Transfer);
2758
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
2759
+ assert(self.scan_lookup_buffer_index >= scan_size + offset);
2760
+ defer offset += scan_size;
2761
+
2762
+ break :size self.execute_query_transfers(
2763
+ batch,
2764
+ self.scan_lookup_buffer[offset..][0..scan_size],
2765
+ encoder_output_buffer,
2766
+ );
2767
+ },
2768
+ .query_accounts => size: {
2769
+ const scan_size: u32 = result_count * @sizeOf(Account);
2770
+ assert(self.scan_lookup_buffer_index <= self.scan_lookup_buffer.len);
2771
+ assert(self.scan_lookup_buffer_index >= scan_size + offset);
2772
+ defer offset += scan_size;
2773
+
2774
+ break :size self.execute_query_accounts(
2775
+ batch,
2776
+ self.scan_lookup_buffer[offset..][0..scan_size],
2777
+ encoder_output_buffer,
2778
+ );
2779
+ },
2780
+ else => comptime unreachable,
2781
+ };
2782
+ maybe(bytes_written == 0);
2783
+ reply_encoder.add(@intCast(bytes_written));
2784
+ }
2785
+ assert(body_decoder.pop() == null);
2786
+ assert(reply_encoder.batch_count == self.scan_lookup_results.items.len);
2787
+ assert(offset == self.scan_lookup_buffer_index);
2788
+
2789
+ const encoded_bytes_written: usize = reply_encoder.finish();
2790
+ assert(encoded_bytes_written > 0);
2791
+ return encoded_bytes_written;
2792
+ }
2793
+
2794
+ pub fn compact(
2795
+ self: *StateMachine,
2796
+ callback: *const fn (*StateMachine) void,
2797
+ op: u64,
2798
+ ) void {
2799
+ assert(self.compact_callback == null);
2800
+ assert(self.checkpoint_callback == null);
2801
+
2802
+ self.metrics.timer.reset();
2803
+
2804
+ self.compact_callback = callback;
2805
+ self.forest.compact(compact_finish, op);
2806
+ }
2807
+
2808
+ fn compact_finish(forest: *Forest) void {
2809
+ const self: *StateMachine = @alignCast(@fieldParentPtr("forest", forest));
2810
+ const callback = self.compact_callback.?;
2811
+ self.compact_callback = null;
2812
+
2813
+ const duration = self.metrics.timer.read();
2814
+ self.metrics.record(.compact, duration.to_us(), 1);
2815
+
2816
+ callback(self);
2817
+ }
2818
+
2819
+ pub fn checkpoint(self: *StateMachine, callback: *const fn (*StateMachine) void) void {
2820
+ assert(self.compact_callback == null);
2821
+ assert(self.checkpoint_callback == null);
2822
+
2823
+ self.metrics.timer.reset();
2824
+
2825
+ self.checkpoint_callback = callback;
2826
+ self.forest.checkpoint(checkpoint_finish);
2827
+ }
2828
+
2829
+ fn checkpoint_finish(forest: *Forest) void {
2830
+ const self: *StateMachine = @alignCast(@fieldParentPtr("forest", forest));
2831
+ const callback = self.checkpoint_callback.?;
2832
+ self.checkpoint_callback = null;
2833
+
2834
+ const duration = self.metrics.timer.read();
2835
+ self.metrics.record(.checkpoint, duration.to_us(), 1);
2836
+
2837
+ self.metrics.log_and_reset();
2838
+
2839
+ callback(self);
2840
+ }
2841
+
2842
+ fn scope_open(self: *StateMachine, operation: Operation) void {
2843
+ switch (operation) {
2844
+ .create_accounts,
2845
+ .deprecated_create_accounts_unbatched,
2846
+ => {
2847
+ self.forest.grooves.accounts.scope_open();
2848
+ },
2849
+ .create_transfers,
2850
+ .deprecated_create_transfers_unbatched,
2851
+ => {
2852
+ self.forest.grooves.accounts.scope_open();
2853
+ self.forest.grooves.transfers.scope_open();
2854
+ self.forest.grooves.transfers_pending.scope_open();
2855
+ self.forest.grooves.account_events.scope_open();
2856
+ },
2857
+ else => unreachable,
2858
+ }
2859
+ }
2860
+
2861
+ fn scope_close(self: *StateMachine, operation: Operation, mode: ScopeCloseMode) void {
2862
+ switch (operation) {
2863
+ .create_accounts,
2864
+ .deprecated_create_accounts_unbatched,
2865
+ => {
2866
+ self.forest.grooves.accounts.scope_close(mode);
2867
+ },
2868
+ .create_transfers,
2869
+ .deprecated_create_transfers_unbatched,
2870
+ => {
2871
+ self.forest.grooves.accounts.scope_close(mode);
2872
+ self.forest.grooves.transfers.scope_close(mode);
2873
+ self.forest.grooves.transfers_pending.scope_close(mode);
2874
+ self.forest.grooves.account_events.scope_close(mode);
2875
+ },
2876
+ else => unreachable,
2877
+ }
2878
+ }
2879
+
2880
+ fn execute_create(
2881
+ self: *StateMachine,
2882
+ comptime operation: Operation,
2883
+ timestamp: u64,
2884
+ batch: []const u8,
2885
+ output_buffer: []u8,
2886
+ ) usize {
2887
+ comptime assert(operation == .create_accounts or
2888
+ operation == .create_transfers or
2889
+ operation == .deprecated_create_accounts_unbatched or
2890
+ operation == .deprecated_create_transfers_unbatched);
2891
+
2892
+ const Event = operation.EventType();
2893
+ const Result = operation.ResultType();
2894
+ const events = stdx.bytes_as_slice(.exact, Event, batch);
2895
+ const results = stdx.bytes_as_slice(.inexact, Result, output_buffer);
2896
+ assert(events.len <= results.len);
2897
+
2898
+ var count: usize = 0;
2899
+ var chain: ?usize = null;
2900
+ var chain_broken = false;
2901
+
2902
+ // The first event determines the batch behavior for
2903
+ // importing events with past timestamp.
2904
+ const batch_imported = events.len > 0 and events[0].flags.imported;
2905
+ for (events, 0..) |*event, index| {
2906
+ const result = result: {
2907
+ if (event.flags.linked) {
2908
+ if (chain == null) {
2909
+ chain = index;
2910
+ assert(chain_broken == false);
2911
+ self.scope_open(operation);
2912
+ }
2913
+
2914
+ if (index == events.len - 1) break :result .linked_event_chain_open;
2915
+ }
2916
+
2917
+ if (chain_broken) break :result .linked_event_failed;
2918
+
2919
+ if (batch_imported != event.flags.imported) {
2920
+ if (event.flags.imported) {
2921
+ break :result .imported_event_not_expected;
2922
+ } else {
2923
+ break :result .imported_event_expected;
2924
+ }
2925
+ }
2926
+
2927
+ const timestamp_event = timestamp: {
2928
+ if (event.flags.imported) {
2929
+ if (!TimestampRange.valid(event.timestamp)) {
2930
+ break :result .imported_event_timestamp_out_of_range;
2931
+ }
2932
+ if (event.timestamp >= timestamp) {
2933
+ break :result .imported_event_timestamp_must_not_advance;
2934
+ }
2935
+ break :timestamp event.timestamp;
2936
+ }
2937
+ if (event.timestamp != 0) break :result .timestamp_must_be_zero;
2938
+ break :timestamp timestamp - events.len + index + 1;
2939
+ };
2940
+ assert(TimestampRange.valid(timestamp_event));
2941
+
2942
+ break :result switch (operation) {
2943
+ .deprecated_create_accounts_unbatched,
2944
+ .create_accounts,
2945
+ => self.create_account(timestamp_event, event),
2946
+ .deprecated_create_transfers_unbatched,
2947
+ .create_transfers,
2948
+ => self.create_transfer(timestamp_event, event),
2949
+ else => comptime unreachable,
2950
+ };
2951
+ };
2952
+ if (self.log_trace) {
2953
+ log.debug("{?}: {s} {}/{}: {}: {}", .{
2954
+ self.forest.grid.superblock.replica_index,
2955
+ @tagName(operation),
2956
+ index + 1,
2957
+ events.len,
2958
+ result,
2959
+ event,
2960
+ });
2961
+ }
2962
+ if (result != .ok) {
2963
+ if (chain) |chain_start_index| {
2964
+ if (!chain_broken) {
2965
+ chain_broken = true;
2966
+ // Our chain has just been broken, discard the scope we started above.
2967
+ self.scope_close(operation, .discard);
2968
+
2969
+ // Add errors for rolled back events in FIFO order:
2970
+ var chain_index = chain_start_index;
2971
+ while (chain_index < index) : (chain_index += 1) {
2972
+ results[count] = .{
2973
+ .index = @intCast(chain_index),
2974
+ .result = .linked_event_failed,
2975
+ };
2976
+ count += 1;
2977
+ }
2978
+ } else {
2979
+ assert(result == .linked_event_failed or
2980
+ result == .linked_event_chain_open);
2981
+ }
2982
+ }
2983
+ results[count] = .{ .index = @intCast(index), .result = result };
2984
+ count += 1;
2985
+
2986
+ self.transient_error(operation, event.id, result);
2987
+ }
2988
+ if (chain != null and (!event.flags.linked or result == .linked_event_chain_open)) {
2989
+ if (!chain_broken) {
2990
+ // We've finished this linked chain, and all events have applied
2991
+ // successfully.
2992
+ self.scope_close(operation, .persist);
2993
+ }
2994
+
2995
+ chain = null;
2996
+ chain_broken = false;
2997
+ }
2998
+ }
2999
+ assert(chain == null);
3000
+ assert(chain_broken == false);
3001
+
3002
+ return @sizeOf(Result) * count;
3003
+ }
3004
+
3005
+ fn transient_error(
3006
+ self: *StateMachine,
3007
+ comptime operation: Operation,
3008
+ id: u128,
3009
+ result: anytype,
3010
+ ) void {
3011
+ assert(result != .ok);
3012
+
3013
+ switch (operation) {
3014
+ .create_accounts,
3015
+ .deprecated_create_accounts_unbatched,
3016
+ => {
3017
+ comptime assert(@TypeOf(result) == CreateAccountResult);
3018
+ // The `create_accounts` error codes do not depend on transient system status.
3019
+ return;
3020
+ },
3021
+ .create_transfers,
3022
+ .deprecated_create_transfers_unbatched,
3023
+ => {
3024
+ comptime assert(@TypeOf(result) == CreateTransferResult);
3025
+
3026
+ // Transfers that fail with transient codes cannot reuse the same `id`,
3027
+ // ensuring strong idempotency guarantees.
3028
+ // Once a transfer fails with a transient error, it must be retried
3029
+ // with a different `id`.
3030
+ if (result.transient()) {
3031
+ self.forest.grooves.transfers.insert_orphaned_id(id);
3032
+ }
3033
+ },
3034
+ else => comptime unreachable,
3035
+ }
3036
+ }
3037
+
3038
+ // Accounts that do not fit in the response are omitted.
3039
+ fn execute_lookup_accounts(
3040
+ self: *StateMachine,
3041
+ batch: []const u8,
3042
+ output_buffer: []u8,
3043
+ ) usize {
3044
+ const events = stdx.bytes_as_slice(.exact, u128, batch);
3045
+ const results = stdx.bytes_as_slice(.inexact, Account, output_buffer);
3046
+ assert(events.len <= results.len);
3047
+
3048
+ var results_count: usize = 0;
3049
+ for (events) |id| {
3050
+ if (self.get_account(id)) |account| {
3051
+ results[results_count] = account;
3052
+ results_count += 1;
3053
+ }
3054
+ }
3055
+ return results_count * @sizeOf(Account);
3056
+ }
3057
+
3058
+ // Transfers that do not fit in the response are omitted.
3059
+ fn execute_lookup_transfers(
3060
+ self: *StateMachine,
3061
+ batch: []const u8,
3062
+ output_buffer: []u8,
3063
+ ) usize {
3064
+ const events = stdx.bytes_as_slice(.exact, u128, batch);
3065
+ const results = stdx.bytes_as_slice(.inexact, Transfer, output_buffer);
3066
+ assert(events.len <= results.len);
3067
+
3068
+ var results_count: usize = 0;
3069
+ for (events) |id| {
3070
+ if (self.get_transfer(id)) |result| {
3071
+ results[results_count] = result;
3072
+ results_count += 1;
3073
+ }
3074
+ }
3075
+ return results_count * @sizeOf(Transfer);
3076
+ }
3077
+
3078
+ fn execute_get_account_transfers(
3079
+ self: *StateMachine,
3080
+ batch: []const u8,
3081
+ scan_buffer: []const u8,
3082
+ output_buffer: []u8,
3083
+ ) usize {
3084
+ _ = self;
3085
+ _ = batch;
3086
+ assert(scan_buffer.len <= output_buffer.len);
3087
+ stdx.copy_disjoint(
3088
+ .inexact,
3089
+ u8,
3090
+ output_buffer,
3091
+ scan_buffer,
3092
+ );
3093
+ return scan_buffer.len;
3094
+ }
3095
+
3096
+ fn execute_get_account_balances(
3097
+ self: *StateMachine,
3098
+ batch: []const u8,
3099
+ scan_buffer: []const u8,
3100
+ output_buffer: []u8,
3101
+ ) usize {
3102
+ _ = self;
3103
+ const scan_count = @divExact(scan_buffer.len, @sizeOf(AccountEvent));
3104
+ const output_count_max = @divFloor(output_buffer.len, @sizeOf(AccountBalance));
3105
+ assert(scan_count <= output_count_max);
3106
+
3107
+ const filter: *const AccountFilter = @alignCast(std.mem.bytesAsValue(
3108
+ AccountFilter,
3109
+ batch,
3110
+ ));
3111
+
3112
+ const scan_results = stdx.bytes_as_slice(.exact, AccountEvent, scan_buffer);
3113
+ const output_slice = stdx.bytes_as_slice(.inexact, AccountBalance, output_buffer);
3114
+ var output_count: u32 = 0;
3115
+
3116
+ for (scan_results) |*result| {
3117
+ assert(result.dr_account_id != result.cr_account_id);
3118
+
3119
+ output_slice[output_count] = if (filter.account_id == result.dr_account_id) .{
3120
+ .timestamp = result.timestamp,
3121
+ .debits_pending = result.dr_debits_pending,
3122
+ .debits_posted = result.dr_debits_posted,
3123
+ .credits_pending = result.dr_credits_pending,
3124
+ .credits_posted = result.dr_credits_posted,
3125
+ } else if (filter.account_id == result.cr_account_id) .{
3126
+ .timestamp = result.timestamp,
3127
+ .debits_pending = result.cr_debits_pending,
3128
+ .debits_posted = result.cr_debits_posted,
3129
+ .credits_pending = result.cr_credits_pending,
3130
+ .credits_posted = result.cr_credits_posted,
3131
+ } else {
3132
+ // We have checked that this account has `flags.history == true`.
3133
+ unreachable;
3134
+ };
3135
+
3136
+ output_count += 1;
3137
+ }
3138
+
3139
+ assert(output_count == scan_results.len);
3140
+ return output_count * @sizeOf(AccountBalance);
3141
+ }
3142
+
3143
+ fn execute_query_accounts(
3144
+ self: *StateMachine,
3145
+ batch: []const u8,
3146
+ scan_buffer: []const u8,
3147
+ output_buffer: []u8,
3148
+ ) usize {
3149
+ _ = self;
3150
+ _ = batch;
3151
+ assert(scan_buffer.len <= output_buffer.len);
3152
+ stdx.copy_disjoint(
3153
+ .inexact,
3154
+ u8,
3155
+ output_buffer,
3156
+ scan_buffer,
3157
+ );
3158
+ return scan_buffer.len;
3159
+ }
3160
+
3161
+ fn execute_query_transfers(
3162
+ self: *StateMachine,
3163
+ batch: []const u8,
3164
+ scan_buffer: []const u8,
3165
+ output_buffer: []u8,
3166
+ ) usize {
3167
+ _ = self;
3168
+ _ = batch;
3169
+ assert(scan_buffer.len <= output_buffer.len);
3170
+ stdx.copy_disjoint(
3171
+ .inexact,
3172
+ u8,
3173
+ output_buffer,
3174
+ scan_buffer,
3175
+ );
3176
+ return scan_buffer.len;
3177
+ }
3178
+
3179
+ fn execute_get_change_events(
3180
+ self: *StateMachine,
3181
+ batch: []const u8,
3182
+ scan_buffer: []const u8,
3183
+ output_buffer: []u8,
3184
+ ) usize {
3185
+ _ = batch;
3186
+
3187
+ const scan_results: []const AccountEvent = stdx.bytes_as_slice(
3188
+ .exact,
3189
+ AccountEvent,
3190
+ scan_buffer,
3191
+ );
3192
+ const output_slice = stdx.bytes_as_slice(.inexact, ChangeEvent, output_buffer);
3193
+ var output_count: u32 = 0;
3194
+
3195
+ for (scan_results) |*result| {
3196
+ assert(TimestampRange.valid(result.timestamp));
3197
+ assert(result.dr_account_id != result.cr_account_id);
3198
+ output_slice[output_count] = switch (result.schema()) {
3199
+ .current => self.get_change_event(result),
3200
+ .former => |former| self.get_change_event_former(former),
3201
+ };
3202
+ output_count += 1;
3203
+ }
3204
+
3205
+ return output_count * @sizeOf(ChangeEvent);
3206
+ }
3207
+
3208
+ fn get_change_event(
3209
+ self: *StateMachine,
3210
+ result: *const AccountEvent,
3211
+ ) ChangeEvent {
3212
+ // Getting the transfer by `timestamp`,
3213
+ // except for expiries where there is no transfer associated with the timestamp.
3214
+ const transfer: Transfer = switch (result.transfer_pending_status) {
3215
+ .none,
3216
+ .pending,
3217
+ .posted,
3218
+ .voided,
3219
+ => switch (self.forest.grooves.transfers.get_by_timestamp(result.timestamp)) {
3220
+ .found_object => |transfer| transfer,
3221
+ .found_orphaned_id, .not_found => unreachable,
3222
+ },
3223
+ .expired => self.get_transfer(result.transfer_pending_id).?,
3224
+ };
3225
+ const dr_account = self.get_account(result.dr_account_id).?;
3226
+ const cr_account = self.get_account(result.cr_account_id).?;
3227
+ assert(transfer.debit_account_id == dr_account.id);
3228
+ assert(transfer.credit_account_id == cr_account.id);
3229
+ assert(transfer.ledger == result.ledger);
3230
+ assert(dr_account.ledger == result.ledger);
3231
+ assert(cr_account.ledger == result.ledger);
3232
+
3233
+ const event_type: ChangeEventType = event_type: {
3234
+ switch (result.transfer_pending_status) {
3235
+ .none => {
3236
+ assert(transfer.timestamp == result.timestamp);
3237
+ assert(!transfer.flags.pending);
3238
+ assert(!transfer.flags.post_pending_transfer);
3239
+ assert(!transfer.flags.void_pending_transfer);
3240
+ assert(transfer.pending_id == 0);
3241
+ break :event_type .single_phase;
3242
+ },
3243
+ .pending => {
3244
+ assert(transfer.timestamp == result.timestamp);
3245
+ assert(transfer.flags.pending);
3246
+ assert(transfer.pending_id == 0);
3247
+ break :event_type .two_phase_pending;
3248
+ },
3249
+ .posted => {
3250
+ assert(transfer.timestamp == result.timestamp);
3251
+ assert(transfer.flags.post_pending_transfer);
3252
+ assert(transfer.pending_id == result.transfer_pending_id);
3253
+ break :event_type .two_phase_posted;
3254
+ },
3255
+ .voided => {
3256
+ assert(transfer.timestamp == result.timestamp);
3257
+ assert(transfer.flags.void_pending_transfer);
3258
+ assert(transfer.pending_id == result.transfer_pending_id);
3259
+ break :event_type .two_phase_voided;
3260
+ },
3261
+ .expired => {
3262
+ assert(transfer.flags.pending);
3263
+ assert(transfer.id == result.transfer_pending_id);
3264
+ assert(transfer.timeout > 0);
3265
+ assert(transfer.timestamp < result.timestamp);
3266
+ break :event_type .two_phase_expired;
3267
+ },
3268
+ }
3269
+ };
3270
+
3271
+ return .{
3272
+ .transfer_id = transfer.id,
3273
+ .transfer_amount = result.amount,
3274
+ .transfer_pending_id = transfer.pending_id,
3275
+ .transfer_user_data_128 = transfer.user_data_128,
3276
+ .transfer_user_data_64 = transfer.user_data_64,
3277
+ .transfer_user_data_32 = transfer.user_data_32,
3278
+ .transfer_timeout = transfer.timeout,
3279
+
3280
+ .ledger = result.ledger,
3281
+ .transfer_code = transfer.code,
3282
+ .transfer_flags = transfer.flags,
3283
+ .type = event_type,
3284
+
3285
+ .debit_account_id = dr_account.id,
3286
+ .debit_account_debits_pending = result.dr_debits_pending,
3287
+ .debit_account_debits_posted = result.dr_debits_posted,
3288
+ .debit_account_credits_pending = result.dr_credits_pending,
3289
+ .debit_account_credits_posted = result.dr_credits_posted,
3290
+ .debit_account_user_data_128 = dr_account.user_data_128,
3291
+ .debit_account_user_data_64 = dr_account.user_data_64,
3292
+ .debit_account_user_data_32 = dr_account.user_data_32,
3293
+ .debit_account_code = dr_account.code,
3294
+ .debit_account_flags = result.dr_account_flags,
3295
+
3296
+ .credit_account_id = cr_account.id,
3297
+ .credit_account_debits_pending = result.cr_debits_pending,
3298
+ .credit_account_debits_posted = result.cr_debits_posted,
3299
+ .credit_account_credits_pending = result.cr_credits_pending,
3300
+ .credit_account_credits_posted = result.cr_credits_posted,
3301
+ .credit_account_user_data_128 = cr_account.user_data_128,
3302
+ .credit_account_user_data_64 = cr_account.user_data_64,
3303
+ .credit_account_user_data_32 = cr_account.user_data_32,
3304
+ .credit_account_code = cr_account.code,
3305
+ .credit_account_flags = result.cr_account_flags,
3306
+
3307
+ .timestamp = result.timestamp,
3308
+ .transfer_timestamp = transfer.timestamp,
3309
+ .debit_account_timestamp = dr_account.timestamp,
3310
+ .credit_account_timestamp = cr_account.timestamp,
3311
+ };
3312
+ }
3313
+
3314
+ fn get_change_event_former(
3315
+ self: *StateMachine,
3316
+ result: *const AccountEvent.Former,
3317
+ ) ChangeEvent {
3318
+ assert(result.dr_account_id != 0);
3319
+ assert(result.cr_account_id != 0);
3320
+ const transfer: Transfer =
3321
+ switch (self.forest.grooves.transfers.get_by_timestamp(result.timestamp)) {
3322
+ .found_object => |transfer| transfer,
3323
+ .found_orphaned_id, .not_found => unreachable,
3324
+ };
3325
+ const dr_account = self.get_account(result.dr_account_id).?;
3326
+ const cr_account = self.get_account(result.cr_account_id).?;
3327
+ assert(transfer.debit_account_id == dr_account.id);
3328
+ assert(transfer.credit_account_id == cr_account.id);
3329
+ assert(transfer.ledger == dr_account.ledger);
3330
+ assert(transfer.ledger == cr_account.ledger);
3331
+
3332
+ const event_type: ChangeEventType = event_type: {
3333
+ if (transfer.flags.pending) break :event_type .two_phase_pending;
3334
+ if (transfer.flags.post_pending_transfer) break :event_type .two_phase_posted;
3335
+ if (transfer.flags.void_pending_transfer) break :event_type .two_phase_voided;
3336
+ break :event_type .single_phase;
3337
+ };
3338
+
3339
+ return .{
3340
+ .transfer_id = transfer.id,
3341
+ .transfer_amount = transfer.amount,
3342
+ .transfer_pending_id = transfer.pending_id,
3343
+ .transfer_user_data_128 = transfer.user_data_128,
3344
+ .transfer_user_data_64 = transfer.user_data_64,
3345
+ .transfer_user_data_32 = transfer.user_data_32,
3346
+ .transfer_timeout = transfer.timeout,
3347
+
3348
+ .ledger = transfer.ledger,
3349
+ .transfer_code = transfer.code,
3350
+
3351
+ .type = event_type,
3352
+
3353
+ .debit_account_id = dr_account.id,
3354
+ .debit_account_debits_pending = result.dr_debits_pending,
3355
+ .debit_account_debits_posted = result.dr_debits_posted,
3356
+ .debit_account_credits_pending = result.dr_credits_pending,
3357
+ .debit_account_credits_posted = result.dr_credits_posted,
3358
+ .debit_account_user_data_128 = dr_account.user_data_128,
3359
+ .debit_account_user_data_64 = dr_account.user_data_64,
3360
+ .debit_account_user_data_32 = dr_account.user_data_32,
3361
+ .debit_account_code = dr_account.code,
3362
+
3363
+ .credit_account_id = cr_account.id,
3364
+ .credit_account_debits_pending = result.cr_debits_pending,
3365
+ .credit_account_debits_posted = result.cr_debits_posted,
3366
+ .credit_account_credits_pending = result.cr_credits_pending,
3367
+ .credit_account_credits_posted = result.cr_credits_posted,
3368
+ .credit_account_user_data_128 = cr_account.user_data_128,
3369
+ .credit_account_user_data_64 = cr_account.user_data_64,
3370
+ .credit_account_user_data_32 = cr_account.user_data_32,
3371
+ .credit_account_code = cr_account.code,
3372
+
3373
+ // Not present in the former schema, returning the most current flags.
3374
+ .transfer_flags = transfer.flags,
3375
+ .debit_account_flags = dr_account.flags,
3376
+ .credit_account_flags = cr_account.flags,
3377
+
3378
+ .timestamp = result.timestamp,
3379
+ .transfer_timestamp = transfer.timestamp,
3380
+ .debit_account_timestamp = dr_account.timestamp,
3381
+ .credit_account_timestamp = cr_account.timestamp,
3382
+ };
3383
+ }
3384
+
3385
+ fn create_account(
3386
+ self: *StateMachine,
3387
+ timestamp: u64,
3388
+ a: *const Account,
3389
+ ) CreateAccountResult {
3390
+ assert(timestamp > self.commit_timestamp or
3391
+ a.flags.imported or
3392
+ self.aof_recovery);
3393
+ if (a.flags.imported) {
3394
+ assert(a.timestamp == timestamp);
3395
+ } else {
3396
+ assert(a.timestamp == 0);
3397
+ }
3398
+
3399
+ if (a.reserved != 0) return .reserved_field;
3400
+ if (a.flags.padding != 0) return .reserved_flag;
3401
+
3402
+ if (a.id == 0) return .id_must_not_be_zero;
3403
+ if (a.id == math.maxInt(u128)) return .id_must_not_be_int_max;
3404
+
3405
+ switch (self.forest.grooves.accounts.get(a.id)) {
3406
+ .found_object => |e| return create_account_exists(a, &e),
3407
+ .found_orphaned_id => unreachable,
3408
+ .not_found => {},
3409
+ }
3410
+
3411
+ if (a.flags.debits_must_not_exceed_credits and a.flags.credits_must_not_exceed_debits) {
3412
+ return .flags_are_mutually_exclusive;
3413
+ }
3414
+
3415
+ if (a.debits_pending != 0) return .debits_pending_must_be_zero;
3416
+ if (a.debits_posted != 0) return .debits_posted_must_be_zero;
3417
+ if (a.credits_pending != 0) return .credits_pending_must_be_zero;
3418
+ if (a.credits_posted != 0) return .credits_posted_must_be_zero;
3419
+ if (a.ledger == 0) return .ledger_must_not_be_zero;
3420
+ if (a.code == 0) return .code_must_not_be_zero;
3421
+
3422
+ if (a.flags.imported) {
3423
+ // Allows past timestamp, but validates whether it regressed from the last
3424
+ // inserted account.
3425
+ // This validation must be called _after_ the idempotency checks so the user
3426
+ // can still handle `exists` results when importing.
3427
+ if (self.forest.grooves.accounts.objects.key_range) |*key_range| {
3428
+ if (timestamp <= key_range.key_max) {
3429
+ return .imported_event_timestamp_must_not_regress;
3430
+ }
3431
+ }
3432
+ if (self.forest.grooves.transfers.exists(timestamp)) {
3433
+ return .imported_event_timestamp_must_not_regress;
3434
+ }
3435
+ }
3436
+
3437
+ self.forest.grooves.accounts.insert(&.{
3438
+ .id = a.id,
3439
+ .debits_pending = 0,
3440
+ .debits_posted = 0,
3441
+ .credits_pending = 0,
3442
+ .credits_posted = 0,
3443
+ .user_data_128 = a.user_data_128,
3444
+ .user_data_64 = a.user_data_64,
3445
+ .user_data_32 = a.user_data_32,
3446
+ .reserved = 0,
3447
+ .ledger = a.ledger,
3448
+ .code = a.code,
3449
+ .flags = a.flags,
3450
+ .timestamp = timestamp,
3451
+ });
3452
+ self.commit_timestamp = timestamp;
3453
+ return .ok;
3454
+ }
3455
+
3456
+ fn create_account_exists(a: *const Account, e: *const Account) CreateAccountResult {
3457
+ assert(a.id == e.id);
3458
+ if (@as(u16, @bitCast(a.flags)) != @as(u16, @bitCast(e.flags))) {
3459
+ return .exists_with_different_flags;
3460
+ }
3461
+ if (a.user_data_128 != e.user_data_128) return .exists_with_different_user_data_128;
3462
+ if (a.user_data_64 != e.user_data_64) return .exists_with_different_user_data_64;
3463
+ if (a.user_data_32 != e.user_data_32) return .exists_with_different_user_data_32;
3464
+ assert(a.reserved == 0 and e.reserved == 0);
3465
+ if (a.ledger != e.ledger) return .exists_with_different_ledger;
3466
+ if (a.code != e.code) return .exists_with_different_code;
3467
+ return .exists;
3468
+ }
3469
+
3470
+ fn create_transfer(
3471
+ self: *StateMachine,
3472
+ timestamp: u64,
3473
+ t: *const Transfer,
3474
+ ) CreateTransferResult {
3475
+ assert(timestamp > self.commit_timestamp or
3476
+ t.flags.imported or
3477
+ self.aof_recovery);
3478
+ if (t.flags.imported) {
3479
+ assert(t.timestamp == timestamp);
3480
+ } else {
3481
+ assert(t.timestamp == 0);
3482
+ }
3483
+
3484
+ if (t.flags.padding != 0) return .reserved_flag;
3485
+
3486
+ if (t.id == 0) return .id_must_not_be_zero;
3487
+ if (t.id == math.maxInt(u128)) return .id_must_not_be_int_max;
3488
+
3489
+ switch (self.forest.grooves.transfers.get(t.id)) {
3490
+ .found_object => |*e| return self.create_transfer_exists(t, e),
3491
+ .found_orphaned_id => return .id_already_failed,
3492
+ .not_found => {},
3493
+ }
3494
+
3495
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
3496
+ return self.post_or_void_pending_transfer(timestamp, t);
3497
+ }
3498
+
3499
+ if (t.debit_account_id == 0) return .debit_account_id_must_not_be_zero;
3500
+ if (t.debit_account_id == math.maxInt(u128)) {
3501
+ return .debit_account_id_must_not_be_int_max;
3502
+ }
3503
+ if (t.credit_account_id == 0) return .credit_account_id_must_not_be_zero;
3504
+ if (t.credit_account_id == math.maxInt(u128)) {
3505
+ return .credit_account_id_must_not_be_int_max;
3506
+ }
3507
+ if (t.credit_account_id == t.debit_account_id) return .accounts_must_be_different;
3508
+
3509
+ if (t.pending_id != 0) return .pending_id_must_be_zero;
3510
+ if (!t.flags.pending) {
3511
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
3512
+ if (t.flags.closing_debit or t.flags.closing_credit) {
3513
+ return .closing_transfer_must_be_pending;
3514
+ }
3515
+ }
3516
+
3517
+ if (t.ledger == 0) return .ledger_must_not_be_zero;
3518
+ if (t.code == 0) return .code_must_not_be_zero;
3519
+
3520
+ // The etymology of the DR and CR abbreviations for debit/credit is interesting, either:
3521
+ // 1. derived from the Latin past participles of debitum/creditum, i.e. debere/credere,
3522
+ // 2. standing for debit record and credit record, or
3523
+ // 3. relating to debtor and creditor.
3524
+ // We use them to distinguish between `cr` (credit account), and `c` (commit).
3525
+ const dr_account = self.get_account(t.debit_account_id) orelse
3526
+ return .debit_account_not_found;
3527
+ const cr_account = self.get_account(t.credit_account_id) orelse
3528
+ return .credit_account_not_found;
3529
+ assert(dr_account.id == t.debit_account_id);
3530
+ assert(cr_account.id == t.credit_account_id);
3531
+
3532
+ if (dr_account.ledger != cr_account.ledger) return .accounts_must_have_the_same_ledger;
3533
+ if (t.ledger != dr_account.ledger) {
3534
+ return .transfer_must_have_the_same_ledger_as_accounts;
3535
+ }
3536
+
3537
+ if (t.flags.imported) {
3538
+ // Allows past timestamp, but validates whether it regressed from the last
3539
+ // inserted event.
3540
+ // This validation must be called _after_ the idempotency checks so the user
3541
+ // can still handle `exists` results when importing.
3542
+ if (self.forest.grooves.transfers.objects.key_range) |*key_range| {
3543
+ if (timestamp <= key_range.key_max) {
3544
+ return .imported_event_timestamp_must_not_regress;
3545
+ }
3546
+ }
3547
+ if (self.forest.grooves.accounts.exists(timestamp)) {
3548
+ return .imported_event_timestamp_must_not_regress;
3549
+ }
3550
+
3551
+ if (timestamp <= dr_account.timestamp) {
3552
+ return .imported_event_timestamp_must_postdate_debit_account;
3553
+ }
3554
+ if (timestamp <= cr_account.timestamp) {
3555
+ return .imported_event_timestamp_must_postdate_credit_account;
3556
+ }
3557
+ if (t.timeout != 0) {
3558
+ assert(t.flags.pending);
3559
+ return .imported_event_timeout_must_be_zero;
3560
+ }
3561
+ }
3562
+ assert(timestamp > dr_account.timestamp);
3563
+ assert(timestamp > cr_account.timestamp);
3564
+
3565
+ if (dr_account.flags.closed) return .debit_account_already_closed;
3566
+ if (cr_account.flags.closed) return .credit_account_already_closed;
3567
+
3568
+ maybe(t.amount == 0);
3569
+ const amount_actual = amount: {
3570
+ var amount = t.amount;
3571
+ if (t.flags.balancing_debit) {
3572
+ const dr_balance = dr_account.debits_posted + dr_account.debits_pending;
3573
+ amount = @min(amount, dr_account.credits_posted -| dr_balance);
3574
+ }
3575
+
3576
+ if (t.flags.balancing_credit) {
3577
+ const cr_balance = cr_account.credits_posted + cr_account.credits_pending;
3578
+ amount = @min(amount, cr_account.debits_posted -| cr_balance);
3579
+ }
3580
+ break :amount amount;
3581
+ };
3582
+ maybe(amount_actual == 0);
3583
+
3584
+ if (t.flags.pending) {
3585
+ if (sum_overflows(u128, amount_actual, dr_account.debits_pending)) {
3586
+ return .overflows_debits_pending;
3587
+ }
3588
+ if (sum_overflows(u128, amount_actual, cr_account.credits_pending)) {
3589
+ return .overflows_credits_pending;
3590
+ }
3591
+ }
3592
+ if (sum_overflows(u128, amount_actual, dr_account.debits_posted)) {
3593
+ return .overflows_debits_posted;
3594
+ }
3595
+ if (sum_overflows(u128, amount_actual, cr_account.credits_posted)) {
3596
+ return .overflows_credits_posted;
3597
+ }
3598
+ // We assert that the sum of the pending and posted balances can never overflow:
3599
+ if (sum_overflows(
3600
+ u128,
3601
+ amount_actual,
3602
+ dr_account.debits_pending + dr_account.debits_posted,
3603
+ )) {
3604
+ return .overflows_debits;
3605
+ }
3606
+ if (sum_overflows(
3607
+ u128,
3608
+ amount_actual,
3609
+ cr_account.credits_pending + cr_account.credits_posted,
3610
+ )) {
3611
+ return .overflows_credits;
3612
+ }
3613
+
3614
+ // Comptime asserts that the max value of the timeout expressed in seconds cannot
3615
+ // overflow a `u63` when converted to nanoseconds.
3616
+ // It is `u63` because the most significant bit of the `u64` timestamp
3617
+ // is used as the tombstone flag.
3618
+ comptime assert(!std.meta.isError(std.math.mul(
3619
+ u63,
3620
+ @as(u63, std.math.maxInt(@TypeOf(t.timeout))),
3621
+ std.time.ns_per_s,
3622
+ )));
3623
+ if (sum_overflows(
3624
+ u63,
3625
+ @intCast(timestamp),
3626
+ @as(u63, t.timeout) * std.time.ns_per_s,
3627
+ )) {
3628
+ return .overflows_timeout;
3629
+ }
3630
+
3631
+ if (dr_account.debits_exceed_credits(amount_actual)) return .exceeds_credits;
3632
+ if (cr_account.credits_exceed_debits(amount_actual)) return .exceeds_debits;
3633
+
3634
+ // After this point, the transfer must succeed.
3635
+ defer assert(self.commit_timestamp == timestamp);
3636
+
3637
+ self.forest.grooves.transfers.insert(&.{
3638
+ .id = t.id,
3639
+ .debit_account_id = t.debit_account_id,
3640
+ .credit_account_id = t.credit_account_id,
3641
+ .amount = amount_actual,
3642
+ .pending_id = t.pending_id,
3643
+ .user_data_128 = t.user_data_128,
3644
+ .user_data_64 = t.user_data_64,
3645
+ .user_data_32 = t.user_data_32,
3646
+ .timeout = t.timeout,
3647
+ .ledger = t.ledger,
3648
+ .code = t.code,
3649
+ .flags = t.flags,
3650
+ .timestamp = timestamp,
3651
+ });
3652
+
3653
+ var dr_account_new = dr_account;
3654
+ var cr_account_new = cr_account;
3655
+ if (t.flags.pending) {
3656
+ dr_account_new.debits_pending += amount_actual;
3657
+ cr_account_new.credits_pending += amount_actual;
3658
+
3659
+ self.forest.grooves.transfers_pending.insert(&.{
3660
+ .timestamp = timestamp,
3661
+ .status = .pending,
3662
+ });
3663
+ } else {
3664
+ dr_account_new.debits_posted += amount_actual;
3665
+ cr_account_new.credits_posted += amount_actual;
3666
+ }
3667
+
3668
+ // Closing accounts:
3669
+ assert(!dr_account_new.flags.closed);
3670
+ assert(!cr_account_new.flags.closed);
3671
+ if (t.flags.closing_debit) dr_account_new.flags.closed = true;
3672
+ if (t.flags.closing_credit) cr_account_new.flags.closed = true;
3673
+
3674
+ const dr_updated = amount_actual > 0 or dr_account_new.flags.closed;
3675
+ assert(dr_updated == !stdx.equal_bytes(Account, &dr_account, &dr_account_new));
3676
+ if (dr_updated) {
3677
+ self.forest.grooves.accounts.update(.{
3678
+ .old = &dr_account,
3679
+ .new = &dr_account_new,
3680
+ });
3681
+ }
3682
+
3683
+ const cr_updated = amount_actual > 0 or cr_account_new.flags.closed;
3684
+ assert(cr_updated == !stdx.equal_bytes(Account, &cr_account, &cr_account_new));
3685
+ if (cr_updated) {
3686
+ self.forest.grooves.accounts.update(.{
3687
+ .old = &cr_account,
3688
+ .new = &cr_account_new,
3689
+ });
3690
+ }
3691
+
3692
+ self.account_event(.{
3693
+ .event_timestamp = timestamp,
3694
+ .dr_account = &dr_account_new,
3695
+ .cr_account = &cr_account_new,
3696
+ .transfer_flags = t.flags,
3697
+ .transfer_pending_status = if (t.flags.pending) .pending else .none,
3698
+ .transfer_pending = null,
3699
+ .amount_requested = t.amount,
3700
+ .amount = amount_actual,
3701
+ });
3702
+
3703
+ if (t.timeout > 0) {
3704
+ assert(t.flags.pending);
3705
+ assert(!t.flags.imported);
3706
+ const expires_at = timestamp + t.timeout_ns();
3707
+ if (expires_at < self.expire_pending_transfers.pulse_next_timestamp) {
3708
+ self.expire_pending_transfers.pulse_next_timestamp = expires_at;
3709
+ }
3710
+ }
3711
+
3712
+ self.commit_timestamp = timestamp;
3713
+ return .ok;
3714
+ }
3715
+
3716
+ fn create_transfer_exists(
3717
+ self: *const StateMachine,
3718
+ t: *const Transfer,
3719
+ e: *const Transfer,
3720
+ ) CreateTransferResult {
3721
+ assert(t.id == e.id);
3722
+ // The flags change the behavior of the remaining comparisons,
3723
+ // so compare the flags first.
3724
+ if (@as(u16, @bitCast(t.flags)) != @as(u16, @bitCast(e.flags))) {
3725
+ return .exists_with_different_flags;
3726
+ }
3727
+
3728
+ if (t.pending_id != e.pending_id) return .exists_with_different_pending_id;
3729
+ if (t.timeout != e.timeout) return .exists_with_different_timeout;
3730
+
3731
+ if (t.flags.post_pending_transfer or t.flags.void_pending_transfer) {
3732
+ // Since both `t` and `e` have the same `pending_id`,
3733
+ // it must be a valid transfer.
3734
+ const p = self.get_transfer(t.pending_id).?;
3735
+ return post_or_void_pending_transfer_exists(t, e, &p);
3736
+ } else {
3737
+ if (t.debit_account_id != e.debit_account_id) {
3738
+ return .exists_with_different_debit_account_id;
3739
+ }
3740
+ if (t.credit_account_id != e.credit_account_id) {
3741
+ return .exists_with_different_credit_account_id;
3742
+ }
3743
+
3744
+ // In transfers with `flags.balancing_debit` or `flags.balancing_credit` set,
3745
+ // the field `amount` means the _upper limit_ (or zero for `maxInt`) that can be
3746
+ // moved in order to balance debits and credits.
3747
+ // The actual amount moved depends on the account's balance at the time the
3748
+ // transfer was executed.
3749
+ //
3750
+ // This is a special case in the idempotency check:
3751
+ // When _resubmitting_ the same balancing transfer, the amount will likely be
3752
+ // different from what was previously committed, but as long as it is within the
3753
+ // range of possible values it should fail with `exists` rather than
3754
+ // `exists_with_different_amount`.
3755
+ if (t.flags.balancing_debit or t.flags.balancing_credit) {
3756
+ if (t.amount < e.amount) return .exists_with_different_amount;
3757
+ } else {
3758
+ if (t.amount != e.amount) return .exists_with_different_amount;
3759
+ }
3760
+
3761
+ if (t.user_data_128 != e.user_data_128) {
3762
+ return .exists_with_different_user_data_128;
3763
+ }
3764
+ if (t.user_data_64 != e.user_data_64) {
3765
+ return .exists_with_different_user_data_64;
3766
+ }
3767
+ if (t.user_data_32 != e.user_data_32) {
3768
+ return .exists_with_different_user_data_32;
3769
+ }
3770
+ if (t.ledger != e.ledger) {
3771
+ return .exists_with_different_ledger;
3772
+ }
3773
+ if (t.code != e.code) {
3774
+ return .exists_with_different_code;
3775
+ }
3776
+
3777
+ return .exists;
3778
+ }
3779
+ }
3780
+
3781
+ fn post_or_void_pending_transfer(
3782
+ self: *StateMachine,
3783
+ timestamp: u64,
3784
+ t: *const Transfer,
3785
+ ) CreateTransferResult {
3786
+ assert(t.id != 0);
3787
+ assert(t.id != std.math.maxInt(u128));
3788
+ assert(self.forest.grooves.transfers.get(t.id) == .not_found);
3789
+ assert(t.flags.padding == 0);
3790
+ assert(timestamp > self.commit_timestamp or t.flags.imported);
3791
+ if (t.flags.imported) {
3792
+ assert(t.timestamp == timestamp);
3793
+ } else {
3794
+ assert(t.timestamp == 0);
3795
+ }
3796
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
3797
+
3798
+ if (t.flags.post_pending_transfer and t.flags.void_pending_transfer) {
3799
+ return .flags_are_mutually_exclusive;
3800
+ }
3801
+ if (t.flags.pending) return .flags_are_mutually_exclusive;
3802
+ if (t.flags.balancing_debit) return .flags_are_mutually_exclusive;
3803
+ if (t.flags.balancing_credit) return .flags_are_mutually_exclusive;
3804
+ if (t.flags.closing_debit) return .flags_are_mutually_exclusive;
3805
+ if (t.flags.closing_credit) return .flags_are_mutually_exclusive;
3806
+
3807
+ if (t.pending_id == 0) return .pending_id_must_not_be_zero;
3808
+ if (t.pending_id == math.maxInt(u128)) return .pending_id_must_not_be_int_max;
3809
+ if (t.pending_id == t.id) return .pending_id_must_be_different;
3810
+ if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
3811
+
3812
+ const p = self.get_transfer(t.pending_id) orelse return .pending_transfer_not_found;
3813
+ assert(p.id == t.pending_id);
3814
+ assert(p.timestamp < timestamp);
3815
+ if (!p.flags.pending) return .pending_transfer_not_pending;
3816
+
3817
+ const dr_account = self.get_account(p.debit_account_id).?;
3818
+ const cr_account = self.get_account(p.credit_account_id).?;
3819
+ assert(dr_account.id == p.debit_account_id);
3820
+ assert(cr_account.id == p.credit_account_id);
3821
+ assert(p.timestamp > dr_account.timestamp);
3822
+ assert(p.timestamp > cr_account.timestamp);
3823
+
3824
+ if (t.debit_account_id > 0 and t.debit_account_id != p.debit_account_id) {
3825
+ return .pending_transfer_has_different_debit_account_id;
3826
+ }
3827
+ if (t.credit_account_id > 0 and t.credit_account_id != p.credit_account_id) {
3828
+ return .pending_transfer_has_different_credit_account_id;
3829
+ }
3830
+ // The user_data field is allowed to differ across pending and posting/voiding
3831
+ // transfers.
3832
+ if (t.ledger > 0 and t.ledger != p.ledger) {
3833
+ return .pending_transfer_has_different_ledger;
3834
+ }
3835
+ if (t.code > 0 and t.code != p.code) return .pending_transfer_has_different_code;
3836
+
3837
+ maybe(t.amount == 0);
3838
+ maybe(p.amount == 0);
3839
+ const amount_actual = amount: {
3840
+ if (t.flags.void_pending_transfer) {
3841
+ break :amount if (t.amount == 0) p.amount else t.amount;
3842
+ } else {
3843
+ break :amount if (t.amount == std.math.maxInt(u128)) p.amount else t.amount;
3844
+ }
3845
+ };
3846
+ maybe(amount_actual == 0);
3847
+
3848
+ if (amount_actual > p.amount) return .exceeds_pending_transfer_amount;
3849
+
3850
+ if (t.flags.void_pending_transfer and amount_actual < p.amount) {
3851
+ return .pending_transfer_has_different_amount;
3852
+ }
3853
+
3854
+ const transfer_pending = self.get_transfer_pending(p.timestamp).?;
3855
+ assert(p.timestamp == transfer_pending.timestamp);
3856
+ switch (transfer_pending.status) {
3857
+ .none => unreachable,
3858
+ .pending => {},
3859
+ .posted => return .pending_transfer_already_posted,
3860
+ .voided => return .pending_transfer_already_voided,
3861
+ .expired => {
3862
+ assert(p.timeout > 0);
3863
+ assert(!p.flags.imported);
3864
+ assert(timestamp >= p.timestamp + p.timeout_ns());
3865
+ return .pending_transfer_expired;
3866
+ },
3867
+ }
3868
+
3869
+ const expires_at: ?u64 = if (p.timeout == 0) null else expires_at: {
3870
+ assert(!p.flags.imported);
3871
+ const expires_at: u64 = p.timestamp + p.timeout_ns();
3872
+ if (expires_at <= timestamp) {
3873
+ // TODO: It's still possible for an operation to see an expired transfer
3874
+ // if there's more than one batch of transfers to expire in a single `pulse`
3875
+ // and the current operation was pipelined before the expiration commits.
3876
+ return .pending_transfer_expired;
3877
+ }
3878
+
3879
+ break :expires_at expires_at;
3880
+ };
3881
+
3882
+ if (t.flags.imported) {
3883
+ // Allows past timestamp, but validates whether it regressed from the last
3884
+ // inserted transfer.
3885
+ // This validation must be called _after_ the idempotency checks so the user
3886
+ // can still handle `exists` results when importing.
3887
+ if (self.forest.grooves.transfers.objects.key_range) |*key_range| {
3888
+ if (timestamp <= key_range.key_max) {
3889
+ return .imported_event_timestamp_must_not_regress;
3890
+ }
3891
+ }
3892
+ if (self.forest.grooves.accounts.exists(timestamp)) {
3893
+ return .imported_event_timestamp_must_not_regress;
3894
+ }
3895
+ }
3896
+ assert(timestamp > dr_account.timestamp);
3897
+ assert(timestamp > cr_account.timestamp);
3898
+
3899
+ // The only movement allowed in a closed account is voiding a pending transfer.
3900
+ if (dr_account.flags.closed and !t.flags.void_pending_transfer) {
3901
+ return .debit_account_already_closed;
3902
+ }
3903
+ if (cr_account.flags.closed and !t.flags.void_pending_transfer) {
3904
+ return .credit_account_already_closed;
3905
+ }
3906
+
3907
+ // After this point, the transfer must succeed.
3908
+ defer assert(self.commit_timestamp == timestamp);
3909
+
3910
+ self.forest.grooves.transfers.insert(&.{
3911
+ .id = t.id,
3912
+ .debit_account_id = p.debit_account_id,
3913
+ .credit_account_id = p.credit_account_id,
3914
+ .user_data_128 = if (t.user_data_128 > 0) t.user_data_128 else p.user_data_128,
3915
+ .user_data_64 = if (t.user_data_64 > 0) t.user_data_64 else p.user_data_64,
3916
+ .user_data_32 = if (t.user_data_32 > 0) t.user_data_32 else p.user_data_32,
3917
+ .ledger = p.ledger,
3918
+ .code = p.code,
3919
+ .pending_id = t.pending_id,
3920
+ .timeout = 0,
3921
+ .timestamp = timestamp,
3922
+ .flags = t.flags,
3923
+ .amount = amount_actual,
3924
+ });
3925
+
3926
+ if (expires_at) |expiry| {
3927
+ assert(expiry > timestamp);
3928
+ // Removing the pending `expires_at` index.
3929
+ self.forest.grooves.transfers.indexes.expires_at.remove(&.{
3930
+ .field = expiry,
3931
+ .timestamp = p.timestamp,
3932
+ });
3933
+
3934
+ // In case the pending transfer's timeout is exactly the one we are using
3935
+ // as flag, we need to zero the value to run the next `pulse`.
3936
+ if (self.expire_pending_transfers.pulse_next_timestamp == expiry) {
3937
+ self.expire_pending_transfers.pulse_next_timestamp =
3938
+ TimestampRange.timestamp_min;
3939
+ }
3940
+ }
3941
+
3942
+ const transfer_pending_status: TransferPendingStatus = status: {
3943
+ if (t.flags.post_pending_transfer) break :status .posted;
3944
+ if (t.flags.void_pending_transfer) break :status .voided;
3945
+ unreachable;
3946
+ };
3947
+ self.transfer_update_pending_status(&transfer_pending, transfer_pending_status);
3948
+
3949
+ var dr_account_new = dr_account;
3950
+ var cr_account_new = cr_account;
3951
+ dr_account_new.debits_pending -= p.amount;
3952
+ cr_account_new.credits_pending -= p.amount;
3953
+
3954
+ if (t.flags.post_pending_transfer) {
3955
+ assert(!p.flags.closing_debit);
3956
+ assert(!p.flags.closing_credit);
3957
+ assert(amount_actual <= p.amount);
3958
+ dr_account_new.debits_posted += amount_actual;
3959
+ cr_account_new.credits_posted += amount_actual;
3960
+ }
3961
+ if (t.flags.void_pending_transfer) {
3962
+ assert(amount_actual == p.amount);
3963
+ // Reverts the closing account operation:
3964
+ if (p.flags.closing_debit) {
3965
+ assert(dr_account.flags.closed);
3966
+ dr_account_new.flags.closed = false;
3967
+ }
3968
+ if (p.flags.closing_credit) {
3969
+ assert(cr_account.flags.closed);
3970
+ cr_account_new.flags.closed = false;
3971
+ }
3972
+ }
3973
+
3974
+ const dr_updated = amount_actual > 0 or p.amount > 0 or
3975
+ dr_account_new.flags.closed != dr_account.flags.closed;
3976
+ assert(dr_updated == !stdx.equal_bytes(Account, &dr_account, &dr_account_new));
3977
+ if (dr_updated) {
3978
+ self.forest.grooves.accounts.update(.{
3979
+ .old = &dr_account,
3980
+ .new = &dr_account_new,
3981
+ });
3982
+ }
3983
+
3984
+ const cr_updated = amount_actual > 0 or p.amount > 0 or
3985
+ cr_account_new.flags.closed != cr_account.flags.closed;
3986
+ assert(cr_updated == !stdx.equal_bytes(Account, &cr_account, &cr_account_new));
3987
+ if (cr_updated) {
3988
+ self.forest.grooves.accounts.update(.{
3989
+ .old = &cr_account,
3990
+ .new = &cr_account_new,
3991
+ });
3992
+ }
3993
+
3994
+ self.account_event(.{
3995
+ .event_timestamp = timestamp,
3996
+ .dr_account = &dr_account_new,
3997
+ .cr_account = &cr_account_new,
3998
+ .transfer_flags = t.flags,
3999
+ .transfer_pending_status = transfer_pending_status,
4000
+ .transfer_pending = &p,
4001
+ .amount_requested = t.amount,
4002
+ .amount = amount_actual,
4003
+ });
4004
+
4005
+ self.commit_timestamp = timestamp;
4006
+
4007
+ return .ok;
4008
+ }
4009
+
4010
+ fn post_or_void_pending_transfer_exists(
4011
+ t: *const Transfer,
4012
+ e: *const Transfer,
4013
+ p: *const Transfer,
4014
+ ) CreateTransferResult {
4015
+ assert(t.id == e.id);
4016
+ assert(t.id != p.id);
4017
+ assert(t.flags.post_pending_transfer or t.flags.void_pending_transfer);
4018
+ assert(@as(u16, @bitCast(t.flags)) == @as(u16, @bitCast(e.flags)));
4019
+ assert(t.pending_id == e.pending_id);
4020
+ assert(t.pending_id == p.id);
4021
+ assert(p.flags.pending);
4022
+ assert(t.timeout == e.timeout);
4023
+ assert(t.timeout == 0);
4024
+ assert(e.debit_account_id == p.debit_account_id);
4025
+ assert(e.credit_account_id == p.credit_account_id);
4026
+ assert(e.ledger == p.ledger);
4027
+ assert(e.code == p.code);
4028
+ assert(e.timestamp > p.timestamp);
4029
+
4030
+ if (t.debit_account_id != 0 and t.debit_account_id != e.debit_account_id) {
4031
+ return .exists_with_different_debit_account_id;
4032
+ }
4033
+ if (t.credit_account_id != 0 and t.credit_account_id != e.credit_account_id) {
4034
+ return .exists_with_different_credit_account_id;
4035
+ }
4036
+
4037
+ if (t.flags.void_pending_transfer) {
4038
+ if (t.amount == 0) {
4039
+ if (e.amount != p.amount) return .exists_with_different_amount;
4040
+ } else {
4041
+ if (t.amount != e.amount) return .exists_with_different_amount;
4042
+ }
4043
+ }
4044
+ if (t.flags.post_pending_transfer) {
4045
+ assert(e.amount <= p.amount);
4046
+ if (t.amount == std.math.maxInt(u128)) {
4047
+ if (e.amount != p.amount) return .exists_with_different_amount;
4048
+ } else {
4049
+ if (t.amount != e.amount) return .exists_with_different_amount;
4050
+ }
4051
+ }
4052
+
4053
+ if (t.user_data_128 == 0) {
4054
+ if (e.user_data_128 != p.user_data_128) {
4055
+ return .exists_with_different_user_data_128;
4056
+ }
4057
+ } else {
4058
+ if (t.user_data_128 != e.user_data_128) {
4059
+ return .exists_with_different_user_data_128;
4060
+ }
4061
+ }
4062
+
4063
+ if (t.user_data_64 == 0) {
4064
+ if (e.user_data_64 != p.user_data_64) {
4065
+ return .exists_with_different_user_data_64;
4066
+ }
4067
+ } else {
4068
+ if (t.user_data_64 != e.user_data_64) {
4069
+ return .exists_with_different_user_data_64;
4070
+ }
4071
+ }
4072
+
4073
+ if (t.user_data_32 == 0) {
4074
+ if (e.user_data_32 != p.user_data_32) {
4075
+ return .exists_with_different_user_data_32;
4076
+ }
4077
+ } else {
4078
+ if (t.user_data_32 != e.user_data_32) {
4079
+ return .exists_with_different_user_data_32;
4080
+ }
4081
+ }
4082
+
4083
+ if (t.ledger != 0 and t.ledger != e.ledger) {
4084
+ return .exists_with_different_ledger;
4085
+ }
4086
+ if (t.code != 0 and t.code != e.code) {
4087
+ return .exists_with_different_code;
4088
+ }
4089
+
4090
+ return .exists;
4091
+ }
4092
+
4093
+ fn account_event(
4094
+ self: *StateMachine,
4095
+ args: struct {
4096
+ event_timestamp: u64,
4097
+ dr_account: *const Account,
4098
+ cr_account: *const Account,
4099
+ transfer_flags: ?TransferFlags,
4100
+ transfer_pending_status: TransferPendingStatus,
4101
+ transfer_pending: ?*const Transfer,
4102
+ /// The amount from the user request.
4103
+ /// It may differ from the recorded `amount` when posting transfers and balancing
4104
+ /// accounts. Always zero for expiry events, as no user request is involved.
4105
+ amount_requested: u128,
4106
+ /// The actual amount recorded in the transfer.
4107
+ amount: u128,
4108
+ },
4109
+ ) void {
4110
+ assert(args.event_timestamp > 0);
4111
+ switch (args.transfer_pending_status) {
4112
+ .none, .pending => {
4113
+ assert(args.transfer_flags != null);
4114
+ assert(args.transfer_pending == null);
4115
+ },
4116
+ .posted, .voided => {
4117
+ assert(args.transfer_flags != null);
4118
+ assert(args.transfer_pending != null);
4119
+ },
4120
+ .expired => {
4121
+ assert(args.transfer_flags == null);
4122
+ assert(args.transfer_pending != null);
4123
+ },
4124
+ }
4125
+
4126
+ // For CDC we always insert the history regardless `Account.flags.history`.
4127
+ self.forest.grooves.account_events.insert(&.{
4128
+ .timestamp = args.event_timestamp,
4129
+
4130
+ .dr_account_id = args.dr_account.id,
4131
+ .dr_account_timestamp = args.dr_account.timestamp,
4132
+ .dr_debits_pending = args.dr_account.debits_pending,
4133
+ .dr_debits_posted = args.dr_account.debits_posted,
4134
+ .dr_credits_pending = args.dr_account.credits_pending,
4135
+ .dr_credits_posted = args.dr_account.credits_posted,
4136
+ .dr_account_flags = args.dr_account.flags,
4137
+
4138
+ .cr_account_id = args.cr_account.id,
4139
+ .cr_account_timestamp = args.cr_account.timestamp,
4140
+ .cr_debits_pending = args.cr_account.debits_pending,
4141
+ .cr_debits_posted = args.cr_account.debits_posted,
4142
+ .cr_credits_pending = args.cr_account.credits_pending,
4143
+ .cr_credits_posted = args.cr_account.credits_posted,
4144
+ .cr_account_flags = args.cr_account.flags,
4145
+
4146
+ .amount_requested = args.amount_requested,
4147
+ .amount = args.amount,
4148
+ .ledger = ledger: {
4149
+ assert(args.dr_account.ledger == args.cr_account.ledger);
4150
+ break :ledger args.dr_account.ledger;
4151
+ },
4152
+
4153
+ .transfer_flags = if (args.transfer_flags) |flags| flags else .{},
4154
+
4155
+ .transfer_pending_id = if (args.transfer_pending) |p| p.id else 0,
4156
+ .transfer_pending_status = args.transfer_pending_status,
4157
+ .transfer_pending_flags = if (args.transfer_pending) |p| p.flags else .{},
4158
+ });
4159
+
4160
+ if (args.dr_account.flags.history) {
4161
+ // Indexing the debit account.
4162
+ self.forest.grooves.account_events.indexes.account_timestamp.put(&.{
4163
+ .timestamp = args.event_timestamp,
4164
+ .field = args.dr_account.timestamp,
4165
+ });
4166
+ }
4167
+ if (args.cr_account.flags.history) {
4168
+ // Indexing the credit account.
4169
+ self.forest.grooves.account_events.indexes.account_timestamp.put(&.{
4170
+ .timestamp = args.event_timestamp,
4171
+ .field = args.cr_account.timestamp,
4172
+ });
4173
+ }
4174
+ }
4175
+
4176
+ fn get_transfer(self: *const StateMachine, id: u128) ?Transfer {
4177
+ return switch (self.forest.grooves.transfers.get(id)) {
4178
+ .found_object => |t| t,
4179
+ .found_orphaned_id, .not_found => null,
4180
+ };
4181
+ }
4182
+
4183
+ fn get_account(self: *const StateMachine, id: u128) ?Account {
4184
+ return switch (self.forest.grooves.accounts.get(id)) {
4185
+ .found_object => |a| a,
4186
+ .found_orphaned_id => unreachable,
4187
+ .not_found => null,
4188
+ };
4189
+ }
4190
+
4191
+ /// Returns whether a pending transfer, if it exists, has already been
4192
+ /// posted,voided, or expired.
4193
+ fn get_transfer_pending(
4194
+ self: *const StateMachine,
4195
+ pending_timestamp: u64,
4196
+ ) ?TransferPending {
4197
+ return switch (self.forest.grooves.transfers_pending.get(pending_timestamp)) {
4198
+ .found_object => |a| a,
4199
+ .found_orphaned_id => unreachable,
4200
+ .not_found => null,
4201
+ };
4202
+ }
4203
+
4204
+ fn transfer_update_pending_status(
4205
+ self: *StateMachine,
4206
+ transfer_pending: *const TransferPending,
4207
+ status: TransferPendingStatus,
4208
+ ) void {
4209
+ assert(transfer_pending.timestamp != 0);
4210
+ assert(transfer_pending.status == .pending);
4211
+ assert(status != .none and status != .pending);
4212
+
4213
+ self.forest.grooves.transfers_pending.update(.{
4214
+ .old = transfer_pending,
4215
+ .new = &.{
4216
+ .timestamp = transfer_pending.timestamp,
4217
+ .status = status,
4218
+ },
4219
+ });
4220
+ }
4221
+
4222
+ fn execute_expire_pending_transfers(self: *StateMachine, timestamp: u64) usize {
4223
+ assert(self.scan_lookup_results.items.len == 1); // No multi-batch.
4224
+ assert(timestamp > self.commit_timestamp or self.aof_recovery);
4225
+
4226
+ defer {
4227
+ self.scan_lookup_buffer_index = 0;
4228
+ self.scan_lookup_results.clearRetainingCapacity();
4229
+ }
4230
+
4231
+ const result_count: u32 = self.scan_lookup_results.items[0];
4232
+ if (result_count == 0) return 0;
4233
+
4234
+ const result_max: u32 = @max(
4235
+ Operation.create_transfers.event_max(self.batch_size_limit),
4236
+ Operation.deprecated_create_transfers_unbatched.event_max(self.batch_size_limit),
4237
+ );
4238
+ assert(result_count <= result_max);
4239
+
4240
+ assert(self.scan_lookup_buffer_index > 0);
4241
+ assert(self.scan_lookup_buffer_index == result_count * @sizeOf(Transfer));
4242
+
4243
+ const transfers_pending = stdx.bytes_as_slice(
4244
+ .exact,
4245
+ Transfer,
4246
+ self.scan_lookup_buffer[0..self.scan_lookup_buffer_index],
4247
+ );
4248
+
4249
+ log.debug("expire_pending_transfers: len={}", .{transfers_pending.len});
4250
+
4251
+ for (transfers_pending, 0..) |*p, index| {
4252
+ assert(p.flags.pending);
4253
+ assert(p.timeout > 0);
4254
+
4255
+ const event_timestamp = timestamp - transfers_pending.len + index + 1;
4256
+ assert(TimestampRange.valid(event_timestamp));
4257
+ assert(self.commit_timestamp < event_timestamp);
4258
+ defer self.commit_timestamp = event_timestamp;
4259
+
4260
+ const expires_at = p.timestamp + p.timeout_ns();
4261
+ assert(expires_at <= event_timestamp);
4262
+
4263
+ const dr_account = self.get_account(
4264
+ p.debit_account_id,
4265
+ ).?;
4266
+ assert(dr_account.debits_pending >= p.amount);
4267
+
4268
+ const cr_account = self.get_account(
4269
+ p.credit_account_id,
4270
+ ).?;
4271
+ assert(cr_account.credits_pending >= p.amount);
4272
+
4273
+ var dr_account_new = dr_account;
4274
+ var cr_account_new = cr_account;
4275
+ dr_account_new.debits_pending -= p.amount;
4276
+ cr_account_new.credits_pending -= p.amount;
4277
+
4278
+ if (p.flags.closing_debit) {
4279
+ assert(dr_account_new.flags.closed);
4280
+ dr_account_new.flags.closed = false;
4281
+ }
4282
+ if (p.flags.closing_credit) {
4283
+ assert(cr_account_new.flags.closed);
4284
+ cr_account_new.flags.closed = false;
4285
+ }
4286
+
4287
+ // Pending transfers can expire in closed accounts.
4288
+ maybe(dr_account_new.flags.closed);
4289
+ maybe(cr_account_new.flags.closed);
4290
+
4291
+ const dr_updated = p.amount > 0 or
4292
+ dr_account_new.flags.closed != dr_account.flags.closed;
4293
+ if (dr_updated) {
4294
+ self.forest.grooves.accounts.update(.{
4295
+ .old = &dr_account,
4296
+ .new = &dr_account_new,
4297
+ });
4298
+ }
4299
+
4300
+ const cr_updated = p.amount > 0 or
4301
+ cr_account_new.flags.closed != cr_account.flags.closed;
4302
+ if (cr_updated) {
4303
+ self.forest.grooves.accounts.update(.{
4304
+ .old = &cr_account,
4305
+ .new = &cr_account_new,
4306
+ });
4307
+ }
4308
+
4309
+ const transfer_pending = self.get_transfer_pending(p.timestamp).?;
4310
+ assert(p.timestamp == transfer_pending.timestamp);
4311
+ assert(transfer_pending.status == .pending);
4312
+ self.transfer_update_pending_status(&transfer_pending, .expired);
4313
+
4314
+ // Removing the `expires_at` index.
4315
+ self.forest.grooves.transfers.indexes.expires_at.remove(&.{
4316
+ .timestamp = p.timestamp,
4317
+ .field = expires_at,
4318
+ });
4319
+
4320
+ self.account_event(.{
4321
+ .event_timestamp = event_timestamp,
4322
+ .dr_account = &dr_account_new,
4323
+ .cr_account = &cr_account_new,
4324
+ .transfer_flags = null,
4325
+ .transfer_pending_status = .expired,
4326
+ .transfer_pending = p,
4327
+ .amount_requested = 0,
4328
+ .amount = p.amount,
4329
+ });
4330
+ }
4331
+
4332
+ // This operation has no output.
4333
+ return 0;
4334
+ }
4335
+
4336
+ pub fn forest_options(options: Options) Forest.GroovesOptions {
4337
+ const prefetch_create_accounts_limit: u32 = @max(
4338
+ Operation.create_accounts.event_max(options.batch_size_limit),
4339
+ Operation.deprecated_create_accounts_unbatched.event_max(options.batch_size_limit),
4340
+ );
4341
+ assert(prefetch_create_accounts_limit > 0);
4342
+ assert(prefetch_create_accounts_limit <= batch_max.create_accounts);
4343
+
4344
+ const prefetch_lookup_accounts_limit: u32 = @max(
4345
+ Operation.lookup_accounts.event_max(options.batch_size_limit),
4346
+ Operation.deprecated_lookup_accounts_unbatched.event_max(options.batch_size_limit),
4347
+ );
4348
+ assert(prefetch_lookup_accounts_limit > 0);
4349
+ assert(prefetch_lookup_accounts_limit <= batch_max.lookup_accounts);
4350
+ assert(prefetch_create_accounts_limit <= batch_max.lookup_accounts);
4351
+
4352
+ const prefetch_create_transfers_limit: u32 = @max(
4353
+ Operation.create_transfers.event_max(options.batch_size_limit),
4354
+ Operation.deprecated_create_transfers_unbatched.event_max(options.batch_size_limit),
4355
+ );
4356
+ assert(prefetch_create_transfers_limit > 0);
4357
+ assert(prefetch_create_transfers_limit <= batch_max.create_transfers);
4358
+
4359
+ const prefetch_lookup_transfers_limit: u32 = @max(
4360
+ Operation.lookup_transfers.event_max(options.batch_size_limit),
4361
+ Operation.deprecated_lookup_transfers_unbatched.event_max(options.batch_size_limit),
4362
+ );
4363
+ assert(prefetch_lookup_transfers_limit > 0);
4364
+ assert(prefetch_lookup_transfers_limit <= batch_max.lookup_transfers);
4365
+ assert(prefetch_create_accounts_limit <= batch_max.lookup_transfers);
4366
+ assert(prefetch_create_transfers_limit <= batch_max.lookup_transfers);
4367
+
4368
+ // Inputs are bounded by the runtime-known `batch_size_limit`,
4369
+ // while replies are only limited by the constant `message_body_size_max`.
4370
+ // This allows more read objects (lookups and queries) than writes (creates).
4371
+ maybe(prefetch_lookup_accounts_limit > prefetch_create_accounts_limit);
4372
+ maybe(prefetch_lookup_transfers_limit > prefetch_create_transfers_limit);
4373
+
4374
+ const tree_values_count_limit = tree_values_count(options.batch_size_limit);
4375
+ return .{
4376
+ .accounts = .{
4377
+ // lookup_account() looks up 1 Account per item.
4378
+ .prefetch_entries_for_read_max = @max(
4379
+ prefetch_lookup_accounts_limit,
4380
+ prefetch_create_accounts_limit,
4381
+ ),
4382
+ .prefetch_entries_for_update_max = @max(
4383
+ prefetch_create_accounts_limit, // create_account
4384
+ 2 * prefetch_create_transfers_limit, // create_transfer dr and cr accounts.
4385
+ ),
4386
+ .cache_entries_max = options.cache_entries_accounts,
4387
+ .tree_options_object = .{
4388
+ .batch_value_count_limit = tree_values_count_limit.accounts.timestamp,
4389
+ },
4390
+ .tree_options_id = .{
4391
+ .batch_value_count_limit = tree_values_count_limit.accounts.id,
4392
+ },
4393
+ .tree_options_index = index_tree_options(
4394
+ AccountsGroove.IndexTreeOptions,
4395
+ tree_values_count_limit.accounts,
4396
+ ),
4397
+ },
4398
+ .transfers = .{
4399
+ // lookup_transfer() looks up 1 Transfer.
4400
+ // create_transfer() looks up at most 1 Transfer for posting/voiding.
4401
+ .prefetch_entries_for_read_max = @max(
4402
+ prefetch_lookup_transfers_limit,
4403
+ prefetch_create_transfers_limit,
4404
+ ),
4405
+ // create_transfer() updates a single Transfer.
4406
+ .prefetch_entries_for_update_max = prefetch_create_transfers_limit,
4407
+ .cache_entries_max = options.cache_entries_transfers,
4408
+ .tree_options_object = .{
4409
+ .batch_value_count_limit = tree_values_count_limit.transfers.timestamp,
4410
+ },
4411
+ .tree_options_id = .{
4412
+ .batch_value_count_limit = tree_values_count_limit.transfers.id,
4413
+ },
4414
+ .tree_options_index = index_tree_options(
4415
+ TransfersGroove.IndexTreeOptions,
4416
+ tree_values_count_limit.transfers,
4417
+ ),
4418
+ },
4419
+ .transfers_pending = .{
4420
+ .prefetch_entries_for_read_max = @max(
4421
+ prefetch_lookup_transfers_limit,
4422
+ prefetch_create_transfers_limit,
4423
+ ),
4424
+ // create_transfer() posts/voids at most one transfer.
4425
+ .prefetch_entries_for_update_max = prefetch_create_transfers_limit,
4426
+ .cache_entries_max = options.cache_entries_transfers_pending,
4427
+ .tree_options_object = .{
4428
+ .batch_value_count_limit = tree_values_count_limit
4429
+ .transfers_pending.timestamp,
4430
+ },
4431
+ .tree_options_id = {},
4432
+ .tree_options_index = index_tree_options(
4433
+ TransfersPendingGroove.IndexTreeOptions,
4434
+ tree_values_count_limit.transfers_pending,
4435
+ ),
4436
+ },
4437
+ .account_events = .{
4438
+ .prefetch_entries_for_read_max = 0,
4439
+ // We don't need to update the history, it's append only.
4440
+ .prefetch_entries_for_update_max = 0,
4441
+ .cache_entries_max = 0,
4442
+ .tree_options_object = .{
4443
+ .batch_value_count_limit = tree_values_count_limit
4444
+ .account_events.timestamp,
4445
+ },
4446
+ .tree_options_id = {},
4447
+ .tree_options_index = index_tree_options(
4448
+ AccountEventsGroove.IndexTreeOptions,
4449
+ tree_values_count_limit.account_events,
4450
+ ),
4451
+ },
4452
+ };
4453
+ }
4454
+
4455
+ fn index_tree_options(
4456
+ comptime IndexTreeOptions: type,
4457
+ batch_limits: anytype,
4458
+ ) IndexTreeOptions {
4459
+ var result: IndexTreeOptions = undefined;
4460
+ inline for (comptime std.meta.fieldNames(IndexTreeOptions)) |field| {
4461
+ @field(result, field) = .{ .batch_value_count_limit = @field(batch_limits, field) };
4462
+ }
4463
+ return result;
4464
+ }
4465
+
4466
+ fn tree_values_count(batch_size_limit: u32) struct {
4467
+ accounts: struct {
4468
+ id: u32,
4469
+ user_data_128: u32,
4470
+ user_data_64: u32,
4471
+ user_data_32: u32,
4472
+ ledger: u32,
4473
+ code: u32,
4474
+ timestamp: u32,
4475
+ imported: u32,
4476
+ closed: u32,
4477
+ },
4478
+ transfers: struct {
4479
+ timestamp: u32,
4480
+ id: u32,
4481
+ debit_account_id: u32,
4482
+ credit_account_id: u32,
4483
+ amount: u32,
4484
+ pending_id: u32,
4485
+ user_data_128: u32,
4486
+ user_data_64: u32,
4487
+ user_data_32: u32,
4488
+ ledger: u32,
4489
+ code: u32,
4490
+ expires_at: u32,
4491
+ imported: u32,
4492
+ closing: u32,
4493
+ },
4494
+ transfers_pending: struct {
4495
+ timestamp: u32,
4496
+ status: u32,
4497
+ },
4498
+ account_events: struct {
4499
+ timestamp: u32,
4500
+ account_timestamp: u32,
4501
+ transfer_pending_status: u32,
4502
+ dr_account_id_expired: u32,
4503
+ cr_account_id_expired: u32,
4504
+ transfer_pending_id_expired: u32,
4505
+ ledger_expired: u32,
4506
+ prunable: u32,
4507
+ },
4508
+ } {
4509
+ assert(batch_size_limit <= constants.message_body_size_max);
4510
+
4511
+ const batch_create_accounts: u32 = @max(
4512
+ Operation.create_accounts.event_max(batch_size_limit),
4513
+ Operation.deprecated_create_accounts_unbatched.event_max(batch_size_limit),
4514
+ );
4515
+ assert(batch_create_accounts > 0);
4516
+ assert(batch_create_accounts <= batch_max.create_accounts);
4517
+
4518
+ const batch_create_transfers: u32 = @max(
4519
+ Operation.create_transfers.event_max(batch_size_limit),
4520
+ Operation.deprecated_create_transfers_unbatched.event_max(batch_size_limit),
4521
+ );
4522
+ assert(batch_create_transfers > 0);
4523
+ assert(batch_create_transfers <= batch_max.create_transfers);
4524
+
4525
+ return .{
4526
+ .accounts = .{
4527
+ .id = batch_create_accounts,
4528
+ .user_data_128 = batch_create_accounts,
4529
+ .user_data_64 = batch_create_accounts,
4530
+ .user_data_32 = batch_create_accounts,
4531
+ .ledger = batch_create_accounts,
4532
+ .code = batch_create_accounts,
4533
+ .imported = batch_create_accounts,
4534
+
4535
+ // Transfers mutate the account balance and the closed flag.
4536
+ // Each transfer modifies two accounts.
4537
+ .timestamp = @max(batch_create_accounts, 2 * batch_create_transfers),
4538
+ .closed = @max(batch_create_accounts, 2 * batch_create_transfers),
4539
+ },
4540
+ .transfers = .{
4541
+ .timestamp = batch_create_transfers,
4542
+ .id = batch_create_transfers,
4543
+ .debit_account_id = batch_create_transfers,
4544
+ .credit_account_id = batch_create_transfers,
4545
+ .amount = batch_create_transfers,
4546
+ .pending_id = batch_create_transfers,
4547
+ .user_data_128 = batch_create_transfers,
4548
+ .user_data_64 = batch_create_transfers,
4549
+ .user_data_32 = batch_create_transfers,
4550
+ .ledger = batch_create_transfers,
4551
+ .code = batch_create_transfers,
4552
+ .expires_at = batch_create_transfers,
4553
+ .imported = batch_create_transfers,
4554
+ .closing = batch_create_transfers,
4555
+ },
4556
+ .transfers_pending = .{
4557
+ // Objects are mutated when the pending transfer is posted/voided/expired.
4558
+ .timestamp = 2 * batch_create_transfers,
4559
+ .status = 2 * batch_create_transfers,
4560
+ },
4561
+ .account_events = .{
4562
+ .timestamp = batch_create_transfers,
4563
+ .account_timestamp = 2 * batch_create_transfers, // dr and cr accounts.
4564
+ .transfer_pending_status = batch_create_transfers,
4565
+ .dr_account_id_expired = batch_create_transfers,
4566
+ .cr_account_id_expired = batch_create_transfers,
4567
+ .transfer_pending_id_expired = batch_create_transfers,
4568
+ .ledger_expired = batch_create_transfers,
4569
+ .prunable = batch_create_transfers,
4570
+ },
4571
+ };
4572
+ }
4573
+ };
4574
+ }
4575
+
4576
+ /// Scans all `Transfers` that already expired at any timestamp.
4577
+ /// A custom evaluator is used to stop at the first result where
4578
+ /// `expires_at > prefetch_timestamp` while updating the next pulse timestamp.
4579
+ /// This way we can achieve the same effect of two conditions with a single scan:
4580
+ /// ```
4581
+ /// WHERE expires_at <= prefetch_timestamp
4582
+ /// UNION
4583
+ /// WHERE expires_at > prefetch_timestamp LIMIT 1
4584
+ /// ```
4585
+ fn ExpirePendingTransfersType(
4586
+ comptime TransfersGroove: type,
4587
+ comptime Storage: type,
4588
+ ) type {
4589
+ return struct {
4590
+ const ExpirePendingTransfers = @This();
4591
+ const ScanRangeType = @import("lsm/scan_range.zig").ScanRangeType;
4592
+ const EvaluateNext = @import("lsm/scan_range.zig").EvaluateNext;
4593
+ const ScanLookupStatus = @import("lsm/scan_lookup.zig").ScanLookupStatus;
4594
+
4595
+ const Tree = @FieldType(TransfersGroove.IndexTrees, "expires_at");
4596
+ const Value = Tree.Table.Value;
4597
+
4598
+ // TODO(zig) Context should be `*ExpirePendingTransfers`,
4599
+ // but its a dependency loop.
4600
+ const Context = struct {};
4601
+ const ScanRange = ScanRangeType(
4602
+ Tree,
4603
+ Storage,
4604
+ *Context,
4605
+ value_next,
4606
+ timestamp_from_value,
4607
+ );
4608
+
4609
+ pub const ScanLookup = ScanLookupType(
4610
+ TransfersGroove,
4611
+ ScanRange,
4612
+ Storage,
4613
+ );
4614
+
4615
+ context: Context = undefined,
4616
+ phase: union(enum) {
4617
+ idle,
4618
+ running: struct {
4619
+ scan: ScanRange,
4620
+ expires_at_max: u64,
4621
+ },
4622
+ } = .idle,
4623
+
4624
+ /// Used by the state machine to determine "when" it needs to execute the expiration logic:
4625
+ /// - When `== timestamp_min`, there may be pending transfers to expire,
4626
+ /// but we need to scan to check.
4627
+ /// - When `== timestamp_max`, there are no pending transfers to expire.
4628
+ /// - Otherwise, this is the timestamp of the next pending transfer expiry.
4629
+ pulse_next_timestamp: u64 = TimestampRange.timestamp_min,
4630
+ value_next_expired_at: ?u64 = null,
4631
+
4632
+ fn reset(self: *ExpirePendingTransfers) void {
4633
+ assert(self.phase == .idle);
4634
+ self.* = .{};
4635
+ }
4636
+
4637
+ fn scan(
4638
+ self: *ExpirePendingTransfers,
4639
+ tree: *Tree,
4640
+ buffer: *const ScanBuffer,
4641
+ filter: struct {
4642
+ snapshot: u64,
4643
+ /// Will fetch transfers expired before this timestamp (inclusive).
4644
+ expires_at_max: u64,
4645
+ },
4646
+ ) *ScanRange {
4647
+ assert(self.phase == .idle);
4648
+ assert(TimestampRange.valid(filter.expires_at_max));
4649
+ maybe(filter.expires_at_max != TimestampRange.timestamp_min and
4650
+ filter.expires_at_max != TimestampRange.timestamp_max and
4651
+ self.pulse_next_timestamp > filter.expires_at_max);
4652
+
4653
+ self.* = .{
4654
+ .pulse_next_timestamp = self.pulse_next_timestamp,
4655
+ .phase = .{ .running = .{
4656
+ .expires_at_max = filter.expires_at_max,
4657
+ .scan = ScanRange.init(
4658
+ &self.context,
4659
+ tree,
4660
+ buffer,
4661
+ filter.snapshot,
4662
+ Tree.Table.key_from_value(&.{
4663
+ .field = TimestampRange.timestamp_min,
4664
+ .timestamp = TimestampRange.timestamp_min,
4665
+ }),
4666
+ Tree.Table.key_from_value(&.{
4667
+ .field = TimestampRange.timestamp_max,
4668
+ .timestamp = TimestampRange.timestamp_max,
4669
+ }),
4670
+ .ascending,
4671
+ ),
4672
+ } },
4673
+ };
4674
+ return &self.phase.running.scan;
4675
+ }
4676
+
4677
+ fn finish(
4678
+ self: *ExpirePendingTransfers,
4679
+ status: ScanLookupStatus,
4680
+ results: []const Transfer,
4681
+ ) void {
4682
+ assert(self.phase == .running);
4683
+ if (self.phase.running.expires_at_max != TimestampRange.timestamp_min and
4684
+ self.phase.running.expires_at_max != TimestampRange.timestamp_max and
4685
+ self.pulse_next_timestamp > self.phase.running.expires_at_max)
4686
+ {
4687
+ assert(results.len == 0);
4688
+ }
4689
+
4690
+ switch (status) {
4691
+ .scan_finished => {
4692
+ if (self.value_next_expired_at == null or
4693
+ self.value_next_expired_at.? <= self.phase.running.expires_at_max)
4694
+ {
4695
+ // There are no more unexpired transfers left to expire in the next pulse.
4696
+ self.pulse_next_timestamp = TimestampRange.timestamp_max;
4697
+ } else {
4698
+ self.pulse_next_timestamp = self.value_next_expired_at.?;
4699
+ }
4700
+ },
4701
+ .buffer_finished => {
4702
+ // There are more transfers to expire than a single batch.
4703
+ assert(self.value_next_expired_at != null);
4704
+ self.pulse_next_timestamp = self.value_next_expired_at.?;
4705
+ },
4706
+ else => unreachable,
4707
+ }
4708
+ self.phase = .idle;
4709
+ self.value_next_expired_at = null;
4710
+ }
4711
+
4712
+ inline fn value_next(context: *Context, value: *const Value) EvaluateNext {
4713
+ const self: *ExpirePendingTransfers = @alignCast(@fieldParentPtr(
4714
+ "context",
4715
+ context,
4716
+ ));
4717
+ assert(self.phase == .running);
4718
+
4719
+ const expires_at: u64 = value.field;
4720
+
4721
+ assert(self.value_next_expired_at == null or
4722
+ self.value_next_expired_at.? <= expires_at);
4723
+
4724
+ self.value_next_expired_at = expires_at;
4725
+
4726
+ return if (expires_at <= self.phase.running.expires_at_max)
4727
+ .include_and_continue
4728
+ else
4729
+ .exclude_and_stop;
4730
+ }
4731
+
4732
+ inline fn timestamp_from_value(context: *Context, value: *const Value) u64 {
4733
+ _ = context;
4734
+ return value.timestamp;
4735
+ }
4736
+ };
4737
+ }
4738
+
4739
+ /// Iterates directly over the `ScanTree` since this query doesn't use secondary indexes.
4740
+ /// No additional lookups are necessary, as the `ScanTree` iteration already yields the
4741
+ /// object values.
4742
+ fn ChangeEventsScanLookupType(
4743
+ comptime AccountEventsGroove: type,
4744
+ comptime Storage: type,
4745
+ ) type {
4746
+ const ScanTreeType = @import("lsm/scan_tree.zig").ScanTreeType;
4747
+
4748
+ return struct {
4749
+ const AccountEventsLookup = @This();
4750
+ const ScanTree = ScanTreeType(
4751
+ void,
4752
+ AccountEventsGroove.ObjectTree,
4753
+ Storage,
4754
+ );
4755
+
4756
+ pub const Callback = *const fn (*AccountEventsLookup, []const AccountEvent) void;
4757
+
4758
+ scan_tree: ScanTree,
4759
+ state: union(enum) {
4760
+ idle,
4761
+ scan: struct {
4762
+ buffer: []AccountEvent,
4763
+ callback: Callback,
4764
+ buffer_produced_len: usize,
4765
+ },
4766
+ finished,
4767
+ },
4768
+
4769
+ fn init(
4770
+ self: *AccountEventsLookup,
4771
+ tree: *AccountEventsGroove.ObjectTree,
4772
+ scan_buffer: *const ScanBuffer,
4773
+ snapshot: u64,
4774
+ timestamp_range: TimestampRange,
4775
+ ) void {
4776
+ self.* = .{
4777
+ .scan_tree = ScanTree.init(
4778
+ tree,
4779
+ scan_buffer,
4780
+ snapshot,
4781
+ timestamp_range.min,
4782
+ timestamp_range.max,
4783
+ .ascending,
4784
+ ),
4785
+ .state = .idle,
4786
+ };
4787
+ }
4788
+
4789
+ fn read(
4790
+ self: *AccountEventsLookup,
4791
+ buffer: []AccountEvent,
4792
+ callback: Callback,
4793
+ ) void {
4794
+ assert(self.state == .idle);
4795
+ assert(self.scan_tree.state == .idle);
4796
+ assert(buffer.len > 0);
4797
+
4798
+ self.state = .{
4799
+ .scan = .{
4800
+ .buffer = buffer,
4801
+ .callback = callback,
4802
+ .buffer_produced_len = 0,
4803
+ },
4804
+ };
4805
+ self.scan_tree.read({}, &scan_read_callback);
4806
+ }
4807
+
4808
+ fn scan_read_callback(_: void, scan_tree: *ScanTree) void {
4809
+ const self: *AccountEventsLookup = @fieldParentPtr("scan_tree", scan_tree);
4810
+ assert(self.state == .scan);
4811
+
4812
+ while (scan_tree.next() catch |err| switch (err) {
4813
+ error.Pending => {
4814
+ self.scan_tree.read({}, &scan_read_callback);
4815
+ return;
4816
+ },
4817
+ }) |object| {
4818
+ assert(self.state.scan.buffer_produced_len < self.state.scan.buffer.len);
4819
+ assert(object.timestamp != 0);
4820
+
4821
+ switch (object.schema()) {
4822
+ .current => {},
4823
+ .former => |former| {
4824
+ // In the former schema:
4825
+ // Only accounts with the `history` flag enabled had their balance stored.
4826
+ assert(former.dr_account_id != 0 or former.cr_account_id != 0);
4827
+ if (former.dr_account_id == 0 or former.cr_account_id == 0) {
4828
+ // Skipping events with the balance of just one side,
4829
+ // as they are not useful for `get_change_events`.
4830
+ continue;
4831
+ }
4832
+ },
4833
+ }
4834
+ assert(object.dr_account_id != 0);
4835
+ assert(object.cr_account_id != 0);
4836
+
4837
+ self.state.scan.buffer[self.state.scan.buffer_produced_len] = object;
4838
+ self.state.scan.buffer_produced_len += 1;
4839
+ if (self.state.scan.buffer_produced_len == self.state.scan.buffer.len) break;
4840
+ }
4841
+
4842
+ const callback = self.state.scan.callback;
4843
+ const results = self.state.scan.buffer[0..self.state.scan.buffer_produced_len];
4844
+ self.state = .finished;
4845
+ callback(self, results);
4846
+ }
4847
+ };
4848
+ }
4849
+
4850
+ fn sum_overflows(comptime Int: type, a: Int, b: Int) bool {
4851
+ comptime assert(Int != comptime_int);
4852
+ comptime assert(Int != comptime_float);
4853
+ _ = std.math.add(Int, a, b) catch return true;
4854
+ return false;
4855
+ }
4856
+
4857
+ fn sum_overflows_test(comptime Int: type) !void {
4858
+ try std.testing.expectEqual(false, sum_overflows(Int, math.maxInt(Int), 0));
4859
+ try std.testing.expectEqual(false, sum_overflows(Int, math.maxInt(Int) - 1, 1));
4860
+ try std.testing.expectEqual(false, sum_overflows(Int, 1, math.maxInt(Int) - 1));
4861
+
4862
+ try std.testing.expectEqual(true, sum_overflows(Int, math.maxInt(Int), 1));
4863
+ try std.testing.expectEqual(true, sum_overflows(Int, 1, math.maxInt(Int)));
4864
+
4865
+ try std.testing.expectEqual(true, sum_overflows(Int, math.maxInt(Int), math.maxInt(Int)));
4866
+ try std.testing.expectEqual(true, sum_overflows(Int, math.maxInt(Int), math.maxInt(Int)));
4867
+ }
4868
+
4869
+ test "sum_overflows" {
4870
+ try sum_overflows_test(u64);
4871
+ try sum_overflows_test(u128);
4872
+ }