tigerbeetle 0.0.34 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/ext/tb_client/extconf.rb +13 -13
- data/ext/tb_client/tigerbeetle/LICENSE +177 -0
- data/ext/tb_client/tigerbeetle/build.zig +2327 -0
- data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
- data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
- data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
- data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
- data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
- data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
- data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
- data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
- data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
- data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
- data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
- data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
- data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
- data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
- data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
- data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
- data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
- data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
- data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
- data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
- data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
- data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
- data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
- data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
- data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
- data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
- data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
- data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
- data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
- data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
- data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
- data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
- data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
- data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
- data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
- data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
- data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
- data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
- data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
- data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
- data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
- data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
- data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
- data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
- data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
- data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
- data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
- data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
- data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
- data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
- data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
- data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
- data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
- data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
- data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
- data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
- data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
- data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
- data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
- data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
- data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
- data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
- data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
- data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
- data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
- data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
- data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
- data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
- data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
- data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
- data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
- data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
- data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
- data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
- data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
- data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
- data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
- data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
- data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
- data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
- data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
- data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
- data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
- data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
- data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
- data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
- data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
- data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
- data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
- data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
- data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
- data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
- data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
- data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
- data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
- data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
- data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
- data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
- data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
- data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
- data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
- data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
- data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
- data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
- data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
- data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
- data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
- data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
- data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
- data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
- data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
- data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
- data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
- data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
- data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
- data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
- data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
- data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
- data/lib/tb_client/shared_lib.rb +12 -5
- data/lib/tigerbeetle/client.rb +1 -1
- data/lib/tigerbeetle/platforms.rb +9 -0
- data/lib/tigerbeetle/version.rb +2 -2
- data/tigerbeetle.gemspec +22 -5
- metadata +242 -3
- data/ext/tb_client/pkg.tar.gz +0 -0
|
@@ -0,0 +1,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
|
+
}
|