From 06644b44569424a38cd2361b415e1eb4e567e684 Mon Sep 17 00:00:00 2001 From: oluwadadepo aderemi <272535+dadepo@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:43:44 +0400 Subject: [PATCH 01/25] Added fuzz for rockdb implementation for ledger --- .github/workflows/check.yml | 172 +-- build.zig | 13 +- scripts/style.py | 1 - src/accountsdb/accounts_file.zig | 6 +- src/accountsdb/bank.zig | 4 +- src/accountsdb/buffer_pool.zig | 1207 ++++++++++++++++++ src/accountsdb/db.zig | 396 +++--- src/accountsdb/download.zig | 201 ++- src/accountsdb/fuzz.zig | 71 +- src/accountsdb/fuzz_snapshot.zig | 16 +- src/accountsdb/index.zig | 41 +- src/accountsdb/lib.zig | 11 +- src/accountsdb/readme.md | 2 +- src/accountsdb/snapshots.zig | 921 ++++++++----- src/benchmarks.zig | 92 +- src/bincode/hashmap.zig | 5 +- src/cmd/cmd.zig | 317 ++--- src/cmd/config.zig | 6 +- src/core/leader_schedule.zig | 32 +- src/fuzz.zig | 3 + src/gossip/helpers.zig | 66 + src/gossip/lib.zig | 1 + src/gossip/service.zig | 7 +- src/ledger/fuzz.zig | 324 +++++ src/ledger/lib.zig | 1 + src/ledger/shred_inserter/shred_inserter.zig | 30 +- src/rpc/server.zig | 31 +- src/shred_network/service.zig | 4 +- src/shred_network/shred_processor.zig | 2 +- src/shred_network/shred_verifier.zig | 8 +- src/sig.zig | 2 + src/sync/reference_counter.zig | 20 + 32 files changed, 3103 insertions(+), 910 deletions(-) create mode 100644 src/accountsdb/buffer_pool.zig create mode 100644 src/gossip/helpers.zig create mode 100644 src/ledger/fuzz.zig diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 977f78082..fba807e32 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,7 +55,7 @@ jobs: - name: test run: | - zig build test -Denable-tsan=true + zig build test -Denable-tsan=true zig build test -Denable-tsan=true -Dblockstore=hashmap -Dfilter=ledger kcov_test: @@ -80,6 +80,12 @@ jobs: run: | wget https://github.com/SimonKagstrom/kcov/releases/download/v42/kcov-amd64.tar.gz sudo tar xf kcov-amd64.tar.gz -C / + + - name: fix kcov dependencies + run: | + cd /usr/lib/x86_64-linux-gnu/ + sudo ln libopcodes-2.42-system.so libopcodes-2.38-system.so || echo libopcodes not found + sudo ln libbfd-2.42-system.so libbfd-2.38-system.so || echo libbfd not found - name: run kcov run: | @@ -170,90 +176,90 @@ jobs: - name: run run: ./zig-out/bin/fuzz allocators 19 10000 - benchmarks: - if: ${{ github.ref != 'refs/heads/main' }} - strategy: - matrix: - os: [ubuntu-latest] - runs-on: ${{matrix.os}} - timeout-minutes: 60 - steps: - - name: checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - name: setup zig - uses: mlugg/setup-zig@v1 - with: - version: 0.13.0 - - name: benchmarks - run: zig build -Doptimize=ReleaseSafe benchmark -- all --metrics + # benchmarks: + # if: ${{ github.ref != 'refs/heads/main' }} + # strategy: + # matrix: + # os: [ubuntu-latest] + # runs-on: ${{matrix.os}} + # timeout-minutes: 60 + # steps: + # - name: checkout + # uses: actions/checkout@v2 + # with: + # submodules: recursive + # - name: setup zig + # uses: mlugg/setup-zig@v1 + # with: + # version: 0.13.0 + # - name: benchmarks + # run: zig build -Doptimize=ReleaseSafe benchmark -- all --metrics - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark + # # Download previous benchmark result from cache (if exists) + # - name: Download previous benchmark data + # uses: actions/cache@v4 + # with: + # path: ./cache + # key: ${{ runner.os }}-benchmark - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - # What benchmark tool the output.txt came from - tool: "customSmallerIsBetter" - # Where the output from the benchmark tool is stored - output-file-path: results/output.json - # Where the previous data file is stored - external-data-json-path: ./cache/benchmark-data.json - # Workflow will fail when an alert happens - fail-on-alert: true - # GitHub API token to make a commit comment - github-token: ${{ secrets.GITHUB_TOKEN }} - # Enable alert commit comment - comment-on-alert: true - # Upload the updated cache file for the next job by actions/cache - # only when running on the main branch - save-data-file: false + # # Run `github-action-benchmark` action + # - name: Store benchmark result + # uses: benchmark-action/github-action-benchmark@v1 + # with: + # # What benchmark tool the output.txt came from + # tool: "customSmallerIsBetter" + # # Where the output from the benchmark tool is stored + # output-file-path: results/output.json + # # Where the previous data file is stored + # external-data-json-path: ./cache/benchmark-data.json + # # Workflow will fail when an alert happens + # fail-on-alert: true + # # GitHub API token to make a commit comment + # github-token: ${{ secrets.GITHUB_TOKEN }} + # # Enable alert commit comment + # comment-on-alert: true + # # Upload the updated cache file for the next job by actions/cache + # # only when running on the main branch + # save-data-file: false - main_benchmarks: - if: ${{ github.ref == 'refs/heads/main' }} - strategy: - matrix: - os: [ubuntu-latest] - runs-on: ${{matrix.os}} - timeout-minutes: 60 - steps: - - name: checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - name: setup zig - uses: mlugg/setup-zig@v1 - with: - version: 0.13.0 - - name: benchmarks - run: zig build -Doptimize=ReleaseSafe benchmark -- all --metrics + # main_benchmarks: + # if: ${{ github.ref == 'refs/heads/main' }} + # strategy: + # matrix: + # os: [ubuntu-latest] + # runs-on: ${{matrix.os}} + # timeout-minutes: 60 + # steps: + # - name: checkout + # uses: actions/checkout@v2 + # with: + # submodules: recursive + # - name: setup zig + # uses: mlugg/setup-zig@v1 + # with: + # version: 0.13.0 + # - name: benchmarks + # run: zig build -Doptimize=ReleaseSafe benchmark -- all --metrics - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark + # # Download previous benchmark result from cache (if exists) + # - name: Download previous benchmark data + # uses: actions/cache@v4 + # with: + # path: ./cache + # key: ${{ runner.os }}-benchmark - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - # What benchmark tool the output.txt came from - tool: "customSmallerIsBetter" - # Where the output from the benchmark tool is stored - output-file-path: results/output.json - # Where the previous data file is stored - external-data-json-path: ./cache/benchmark-data.json - # Workflow will fail when an alert happens - fail-on-alert: true - # Upload the updated cache file for the next job by actions/cache - # only when running on the main branch (see if:) - save-data-file: true + # # Run `github-action-benchmark` action + # - name: Store benchmark result + # uses: benchmark-action/github-action-benchmark@v1 + # with: + # # What benchmark tool the output.txt came from + # tool: "customSmallerIsBetter" + # # Where the output from the benchmark tool is stored + # output-file-path: results/output.json + # # Where the previous data file is stored + # external-data-json-path: ./cache/benchmark-data.json + # # Workflow will fail when an alert happens + # fail-on-alert: true + # # Upload the updated cache file for the next job by actions/cache + # # only when running on the main branch (see if:) + # save-data-file: true diff --git a/build.zig b/build.zig index a8fb42018..00020bedc 100644 --- a/build.zig +++ b/build.zig @@ -161,6 +161,11 @@ pub fn build(b: *Build) void { fuzz_exe.root_module.addImport("zig-network", zig_network_module); fuzz_exe.root_module.addImport("httpz", httpz_mod); fuzz_exe.root_module.addImport("zstd", zstd_mod); + fuzz_exe.root_module.addOptions("build-options", build_options); + switch (blockstore_db) { + .rocksdb => fuzz_exe.root_module.addImport("rocksdb", rocksdb_mod), + .hashmap => {}, + } fuzz_exe.linkLibC(); const fuzz_exe_run = b.addRunArtifact(fuzz_exe); @@ -181,6 +186,7 @@ pub fn build(b: *Build) void { benchmark_exe.root_module.addImport("zig-network", zig_network_module); benchmark_exe.root_module.addImport("httpz", httpz_mod); benchmark_exe.root_module.addImport("zstd", zstd_mod); + benchmark_exe.root_module.addImport("curl", curl_mod); benchmark_exe.root_module.addImport("prettytable", pretty_table_mod); switch (blockstore_db) { .rocksdb => benchmark_exe.root_module.addImport("rocksdb", rocksdb_mod), @@ -236,7 +242,7 @@ fn makeZlsNotInstallAnythingDuringBuildOnSave(b: *Build) void { } } -/// TODO: remove after updating to 0.14, where M3 feature detection is fixed. +/// TODO: remove after updating to 0.14, where M3/M4 feature detection is fixed. /// Ref: https://github.com/ziglang/zig/pull/21116 fn defaultTargetDetectM3() ?std.Target.Query { const builtin = @import("builtin"); @@ -245,17 +251,20 @@ fn defaultTargetDetectM3() ?std.Target.Query { .aarch64, .aarch64_be => {}, else => return null, } - var cpu_family: std.c.CPUFAMILY = undefined; var len: usize = @sizeOf(std.c.CPUFAMILY); std.posix.sysctlbynameZ("hw.cpufamily", &cpu_family, &len, null, 0) catch unreachable; + // Detects M4 as M3 to get around missing C flag translations when passing the target to dependencies. + // https://github.com/Homebrew/brew/blob/64edbe6b7905c47b113c1af9cb1a2009ed57a5c7/Library/Homebrew/extend/os/mac/hardware/cpu.rb#L106 const model: *const std.Target.Cpu.Model = switch (@intFromEnum(cpu_family)) { else => return null, 0x2876f5b5 => &std.Target.aarch64.cpu.apple_a17, // ARM_COLL 0xfa33415e => &std.Target.aarch64.cpu.apple_m3, // ARM_IBIZA 0x5f4dea93 => &std.Target.aarch64.cpu.apple_m3, // ARM_LOBOS 0x72015832 => &std.Target.aarch64.cpu.apple_m3, // ARM_PALMA + 0x6f5129ac => &std.Target.aarch64.cpu.apple_m3, // ARM_DONAN (M4) + 0x17d5b93a => &std.Target.aarch64.cpu.apple_m3, // ARM_BRAVA (M4) }; return .{ diff --git a/scripts/style.py b/scripts/style.py index e32716f15..87ee74e45 100644 --- a/scripts/style.py +++ b/scripts/style.py @@ -165,7 +165,6 @@ def unused_imports(args, files_to_check): "src/transaction_sender/transaction_pool.zig", "src/gossip/ping_pong.zig", "src/accountsdb/download.zig", - "src/accountsdb/fuzz.zig", "src/ledger/reader.zig", "src/rpc/client.zig", "src/accountsdb/index.zig", diff --git a/src/accountsdb/accounts_file.zig b/src/accountsdb/accounts_file.zig index d1dba0b6a..33bf32d9d 100644 --- a/src/accountsdb/accounts_file.zig +++ b/src/accountsdb/accounts_file.zig @@ -107,6 +107,10 @@ pub const AccountInFile = struct { offset = std.mem.alignForward(usize, offset, @sizeOf(u64)); return offset; } + + pub fn writeToBufLen() usize { + return @sizeOf(u64) + @sizeOf(u64) + @sizeOf(Pubkey); + } }; /// on-chain account info about the account @@ -275,7 +279,7 @@ pub const AccountFile = struct { const memory = try std.posix.mmap( null, file_size, - std.posix.PROT.READ | std.posix.PROT.WRITE, + std.posix.PROT.READ, std.posix.MAP{ .TYPE = .PRIVATE }, file.handle, 0, diff --git a/src/accountsdb/bank.zig b/src/accountsdb/bank.zig index b00dcc4e0..c00d7d3a7 100644 --- a/src/accountsdb/bank.zig +++ b/src/accountsdb/bank.zig @@ -5,7 +5,7 @@ const sig = @import("../sig.zig"); const AccountsDB = sig.accounts_db.AccountsDB; const GenesisConfig = sig.accounts_db.GenesisConfig; const BankFields = sig.accounts_db.snapshots.BankFields; -const SnapshotFields = sig.accounts_db.snapshots.SnapshotFields; +const SnapshotManifest = sig.accounts_db.snapshots.Manifest; // TODO: we can likley come up with a better name for this struct /// Analogous to [Bank](https://github.com/anza-xyz/agave/blob/ad0a48c7311b08dbb6c81babaf66c136ac092e79/runtime/src/bank.rs#L718) @@ -77,7 +77,7 @@ test "core.bank: load and validate from test snapshot" { const full_manifest_file = try snapdir.openFile(full_manifest_path.constSlice(), .{}); defer full_manifest_file.close(); - const full_manifest = try SnapshotFields.readFromFile(allocator, full_manifest_file); + const full_manifest = try SnapshotManifest.readFromFile(allocator, full_manifest_file); defer full_manifest.deinit(allocator); // use the genesis to verify loading diff --git a/src/accountsdb/buffer_pool.zig b/src/accountsdb/buffer_pool.zig new file mode 100644 index 000000000..c08732785 --- /dev/null +++ b/src/accountsdb/buffer_pool.zig @@ -0,0 +1,1207 @@ +const std = @import("std"); +const sig = @import("../sig.zig"); +const builtin = @import("builtin"); + +const FileId = sig.accounts_db.accounts_file.FileId; + +/// arbitrarily chosen, I believe >95% of accounts will be <= 512 bytes +const FRAME_SIZE = 512; +const INVALID_FRAME = std.math.maxInt(FrameIndex); +const Frame = [FRAME_SIZE]u8; +/// we can get away with a 32-bit index +const FrameIndex = u32; +const FileOffset = u32; +const FrameOffset = u10; // 0..=FRAME_SIZE + +comptime { + // assert our FRAME_SIZE fits in FrameOffset + const offset: FrameOffset = FRAME_SIZE; + _ = offset; +} + +const LinuxIoMode = enum { + Blocking, + IoUring, +}; +const linux_io_mode: LinuxIoMode = .IoUring; + +const use_io_uring = builtin.os.tag == .linux and linux_io_mode == .IoUring; + +const FileIdFileOffset = packed struct(u64) { + const INVALID: FileIdFileOffset = .{ + .file_id = FileId.fromInt(std.math.maxInt(FileId.Int)), + // disambiguate from 0xAAAA / will trigger asserts as it's not even. + .file_offset = 0xBAAD, + }; + + file_id: FileId, + + /// offset in the file from which the frame begin + /// always a multiple of FRAME_SIZE + file_offset: FileOffset, +}; + +/// Used for obtaining cached reads. +/// +/// Design details: +/// Holds a large number of fixed-size "frames". +/// Frames have an associated reference counter, but this is for tracking open +/// handles - frames may outlive the associated rc. Frames are considered dead +/// when are evicted (which may only happen with 0 open handles). +/// A frame is always alive when it can be found in frame_map; a frame is always +/// dead when it is in free_list. +/// A frame dies when its index is evicted from HierarchicalFifo (inside of +/// evictUnusedFrame). +pub const BufferPool = struct { + pub const FrameMap = std.AutoHashMapUnmanaged(FileIdFileOffset, FrameIndex); + + /// indices of all free frames + /// free frames have a refcount of 0 *and* have been evicted + free_list: AtomicStack(FrameIndex), + + /// uniquely identifies a frame + /// for finding your wanted index + /// TODO: a concurrent hashmap would be more appropriate + frame_map_rw: sig.sync.RwMux(FrameMap), + + frames: []Frame, + frames_metadata: FramesMetadata, + + /// used for eviction to free less popular (rc=0) frames first + eviction_lfu: HierarchicalFIFO, + + /// NOTE: we might want this to be a threadlocal for best performance? I don't think this field is threadsafe + io_uring: if (use_io_uring) std.os.linux.IoUring else void, + + pub fn init( + init_allocator: std.mem.Allocator, + num_frames: u32, + ) !BufferPool { + if (num_frames == 0 or num_frames == 1) return error.InvalidArgument; + + const frames = try init_allocator.alignedAlloc(Frame, std.mem.page_size, num_frames); + errdefer init_allocator.free(frames); + + var frames_metadata = try FramesMetadata.init(init_allocator, num_frames); + errdefer frames_metadata.deinit(init_allocator); + + var free_list = try AtomicStack(FrameIndex).init(init_allocator, num_frames); + errdefer free_list.deinit(init_allocator); + for (0..num_frames) |i| free_list.appendAssumeCapacity(@intCast(i)); + + var io_uring = if (use_io_uring) blk: { + // NOTE: this is pretty much a guess, maybe worth tweaking? + // think this is a bit on the high end, libxev uses 256 + const io_uring_entries = 4096; + + break :blk try std.os.linux.IoUring.init( + io_uring_entries, + 0, + ); + } else {}; + errdefer if (use_io_uring) io_uring.deinit(); + + var frame_map: FrameMap = .{}; + try frame_map.ensureTotalCapacity(init_allocator, num_frames); + errdefer frame_map.deinit(init_allocator); + + const frame_map_rw = sig.sync.RwMux(FrameMap).init(frame_map); + + return .{ + .frames = frames, + .frames_metadata = frames_metadata, + .free_list = free_list, + .frame_map_rw = frame_map_rw, + .eviction_lfu = try HierarchicalFIFO.init(init_allocator, num_frames / 10, num_frames), + .io_uring = io_uring, + }; + } + + pub fn deinit(self: *BufferPool, init_allocator: std.mem.Allocator) void { + init_allocator.free(self.frames); + self.frames_metadata.deinit(init_allocator); + if (use_io_uring) self.io_uring.deinit(); + self.free_list.deinit(init_allocator); + self.eviction_lfu.deinit(init_allocator); + const frame_map, var frame_map_lg = self.frame_map_rw.writeWithLock(); + frame_map.deinit(init_allocator); + frame_map_lg.unlock(); + } + + pub fn computeNumberofFrameIndices( + /// inclusive + file_offset_start: FileOffset, + /// exclusive + file_offset_end: FileOffset, + ) error{InvalidArgument}!u32 { + if (file_offset_start > file_offset_end) return error.InvalidArgument; + if (file_offset_start == file_offset_end) return 0; + + const starting_frame = file_offset_start / FRAME_SIZE; + const ending_frame = (file_offset_end - 1) / FRAME_SIZE; + + return ending_frame - starting_frame + 1; + } + + /// allocates the required amount of indices, sets them all to + /// INVALID_FRAME, overwriting with a valid frame where one is found. + /// INVALID_FRAME indicates that there is no frame in the BufferPool for the + /// given file_id and range. + fn computeFrameIndices( + self: *BufferPool, + file_id: FileId, + allocator: std.mem.Allocator, + /// inclusive + file_offset_start: FileOffset, + /// exclusive + file_offset_end: FileOffset, + ) error{ InvalidArgument, OffsetsOutOfBounds, OutOfMemory }![]FrameIndex { + const n_indices = try computeNumberofFrameIndices(file_offset_start, file_offset_end); + + if (n_indices > self.frames.len) return error.OffsetsOutOfBounds; + + const frame_indices = try allocator.alloc(FrameIndex, n_indices); + for (frame_indices) |*f_idx| f_idx.* = INVALID_FRAME; + + // lookup frame mappings + for (0.., frame_indices) |i, *f_idx| { + const file_offset: FileOffset = @intCast( + (i * FRAME_SIZE) + (file_offset_start - file_offset_start % FRAME_SIZE), + ); + + const key: FileIdFileOffset = .{ + .file_id = file_id, + .file_offset = file_offset, + }; + + const maybe_frame_idx = blk: { + const frame_map, var frame_map_lg = self.frame_map_rw.readWithLock(); + defer frame_map_lg.unlock(); + break :blk frame_map.get(key); + }; + + if (maybe_frame_idx) |frame_idx| f_idx.* = frame_idx; + } + + return frame_indices; + } + + /// On a "new" frame (i.e. freshly read into), set all of its associated metadata + /// TODO: atomics + fn overwriteDeadFrameInfo( + self: *BufferPool, + f_idx: FrameIndex, + file_id: FileId, + frame_aligned_file_offset: FileOffset, + size: FrameOffset, + ) error{CannotOverwriteAliveInfo}!void { + try self.overwriteDeadFrameInfoNoSize(f_idx, file_id, frame_aligned_file_offset); + self.frames_metadata.size[f_idx] = size; + } + + /// Useful if you don't currently know the size. + /// make sure to set the size later (!) + /// TODO: atomics + fn overwriteDeadFrameInfoNoSize( + self: *BufferPool, + f_idx: FrameIndex, + file_id: FileId, + frame_aligned_file_offset: FileOffset, + ) error{CannotOverwriteAliveInfo}!void { + std.debug.assert(frame_aligned_file_offset % FRAME_SIZE == 0); + + if (self.frames_metadata.rc[f_idx].isAlive()) { + // not-found indices should always have 0 active readers + return error.CannotOverwriteAliveInfo; + } + + self.frames_metadata.freq[f_idx] = 0; + self.frames_metadata.in_queue[f_idx] = .none; + self.frames_metadata.rc[f_idx].reset(); + + self.frames_metadata.key[f_idx] = .{ + .file_id = file_id, + .file_offset = frame_aligned_file_offset, + }; + + const key: FileIdFileOffset = .{ + .file_id = file_id, + .file_offset = frame_aligned_file_offset, + }; + + { + const frame_map, var frame_map_lg = self.frame_map_rw.writeWithLock(); + defer frame_map_lg.unlock(); + frame_map.putAssumeCapacityNoClobber(key, f_idx); + } + } + + /// Frames with an associated rc of 0 are up for eviction, and which frames + /// are evicted first is up to the LFU. + fn evictUnusedFrame(self: *BufferPool) error{CannotResetAlive}!void { + const evicted = self.eviction_lfu.evict(self.frames_metadata); + self.free_list.appendAssumeCapacity(evicted); + + const did_remove = blk: { + const frame_map, var frame_map_lg = self.frame_map_rw.writeWithLock(); + defer frame_map_lg.unlock(); + break :blk frame_map.remove(self.frames_metadata.key[evicted]); + }; + if (!did_remove) { + std.debug.panic( + "evicted a frame that did not exist in frame_map, frame: {}\n", + .{evicted}, + ); + } + @memset(&self.frames[evicted], 0xAA); + try self.frames_metadata.resetFrame(evicted); + } + + pub fn read( + self: *BufferPool, + /// used for temp allocations, and the returned .indices slice + allocator: std.mem.Allocator, + file: std.fs.File, + file_id: FileId, + /// inclusive + file_offset_start: FileOffset, + /// exclusive + file_offset_end: FileOffset, + ) !CachedRead { + return if (use_io_uring) + self.readIoUringSubmitAndWait( + allocator, + file, + file_id, + file_offset_start, + file_offset_end, + ) + else + self.readBlocking( + allocator, + file, + file_id, + file_offset_start, + file_offset_end, + ); + } + + fn readIoUringSubmitAndWait( + self: *BufferPool, + /// used for temp allocations, and the returned .indices slice + allocator: std.mem.Allocator, + file: std.fs.File, + file_id: FileId, + /// inclusive + file_offset_start: FileOffset, + /// exclusive + file_offset_end: FileOffset, + ) !CachedRead { + if (!use_io_uring) @compileError("io_uring disabled"); + + const frame_indices = try self.computeFrameIndices( + file_id, + allocator, + file_offset_start, + file_offset_end, + ); + errdefer allocator.free(frame_indices); + + // update found frames in the LFU (we don't want to evict these in the next loop) + var n_invalid_indices: u32 = 0; + for (frame_indices) |f_idx| { + if (f_idx == INVALID_FRAME) { + n_invalid_indices += 1; + continue; + } + try self.eviction_lfu.insert(self.frames_metadata, f_idx); + if (!self.frames_metadata.rc[f_idx].acquire()) { + // frame has no handles, but memory is still valid + self.frames_metadata.rc[f_idx].reset(); + } + } + + // fill in invalid frames with file data, replacing invalid frames with + // fresh ones. + for (0.., frame_indices) |i, *f_idx| { + if (f_idx.* != INVALID_FRAME) continue; + // INVALID_FRAME => not found, read fresh and populate + + const frame_aligned_file_offset: FileOffset = @intCast((i * FRAME_SIZE) + + (file_offset_start - file_offset_start % FRAME_SIZE)); + std.debug.assert(frame_aligned_file_offset % FRAME_SIZE == 0); + + f_idx.* = blk: while (true) { + if (self.free_list.popOrNull()) |free_idx| { + break :blk free_idx; + } else { + try self.evictUnusedFrame(); + } + }; + + _ = try self.io_uring.read( + f_idx.*, + file.handle, + .{ .buffer = &self.frames[f_idx.*] }, + frame_aligned_file_offset, + ); + try self.overwriteDeadFrameInfoNoSize(f_idx.*, file_id, frame_aligned_file_offset); + try self.eviction_lfu.insert(self.frames_metadata, f_idx.*); + } + + // Wait for our file reads to complete, filling the read length into the metadata as we go. + // (This read length will almost always be FRAME_SIZE, however it will likely be less than + // that at the end of the file) + if (n_invalid_indices > 0) { + const n_submitted = try self.io_uring.submit_and_wait(n_invalid_indices); + std.debug.assert(n_submitted == n_invalid_indices); // did smthng else submit an event? + + // would be nice to get rid of this alloc + const cqes = try allocator.alloc(std.os.linux.io_uring_cqe, n_submitted); + defer allocator.free(cqes); + + // check our completions in order to set the frame's size; + // we need to wait for completion to get the bytes read + const cqe_count = try self.io_uring.copy_cqes(cqes, n_submitted); + std.debug.assert(cqe_count == n_submitted); // why did we not receive them all? + for (0.., cqes) |i, cqe| { + if (cqe.err() != .SUCCESS) { + std.debug.panic("cqe err: {}, i: {}", .{ cqe, i }); + } + const f_idx = cqe.user_data; + const bytes_read: FrameOffset = @intCast(cqe.res); + std.debug.assert(bytes_read <= FRAME_SIZE); + + // TODO: atomics + self.frames_metadata.size[f_idx] = bytes_read; + } + } + + return CachedRead{ + .buffer_pool = self, + .frame_indices = frame_indices, + .first_frame_start_offset = @intCast(file_offset_start % FRAME_SIZE), + .last_frame_end_offset = @intCast(((file_offset_end - 1) % FRAME_SIZE) + 1), + }; + } + + fn readBlocking( + self: *BufferPool, + /// used for temp allocations, and the returned .indices slice + allocator: std.mem.Allocator, + file: std.fs.File, + file_id: FileId, + /// inclusive + file_offset_start: FileOffset, + /// exclusive + file_offset_end: FileOffset, + ) (error{ + InvalidArgument, + OutOfMemory, + InvalidKey, + CannotResetAlive, + CannotOverwriteAliveInfo, + OffsetsOutOfBounds, + } || std.posix.PReadError)!CachedRead { + const frame_indices = try self.computeFrameIndices( + file_id, + allocator, + file_offset_start, + file_offset_end, + ); + errdefer allocator.free(frame_indices); + + // update found frames in the LFU (we don't want to evict these in the next loop) + for (frame_indices) |f_idx| { + if (f_idx == INVALID_FRAME) continue; + try self.eviction_lfu.insert(self.frames_metadata, f_idx); + if (!self.frames_metadata.rc[f_idx].acquire()) { + // frame has no handles, but memory is still valid + self.frames_metadata.rc[f_idx].reset(); + } + } + + // fill in invalid frames with file data, replacing invalid frames with + // fresh ones. + for (0.., frame_indices) |i, *f_idx| { + if (f_idx.* != INVALID_FRAME) continue; + // INVALID_FRAME => not found, read fresh and populate + + const frame_aligned_file_offset: FileOffset = @intCast((i * FRAME_SIZE) + + (file_offset_start - file_offset_start % FRAME_SIZE)); + std.debug.assert(frame_aligned_file_offset % FRAME_SIZE == 0); + + f_idx.* = blk: while (true) { + if (self.free_list.popOrNull()) |free_idx| { + break :blk free_idx; + } else { + try self.evictUnusedFrame(); + } + }; + + const bytes_read = try file.pread(&self.frames[f_idx.*], frame_aligned_file_offset); + try self.overwriteDeadFrameInfo( + f_idx.*, + file_id, + frame_aligned_file_offset, + @intCast(bytes_read), + ); + try self.eviction_lfu.insert(self.frames_metadata, f_idx.*); + } + + return CachedRead{ + .buffer_pool = self, + .frame_indices = frame_indices, + .first_frame_start_offset = @intCast(file_offset_start % FRAME_SIZE), + .last_frame_end_offset = @intCast(((file_offset_end - 1) % FRAME_SIZE) + 1), + }; + } +}; + +/// TODO: atomics on all index accesses +pub const FramesMetadata = struct { + pub const InQueue = enum(u2) { none, small, main, ghost }; + + /// ref count for the frame. For frames that are currently being used elsewhere. + rc: []sig.sync.ReferenceCounter, + + /// effectively the inverse of BufferPool.FrameMap, used in order to + /// evict keys by their value + key: []FileIdFileOffset, + + /// frequency for the S3_FIFO + /// Yes, really, only 0, 1, 2, 3. + freq: []u2, + + /// which S3_FIFO queue this frame exists in + in_queue: []InQueue, + + /// 0..=512 + size: []FrameOffset, + + fn init(allocator: std.mem.Allocator, num_frames: usize) !FramesMetadata { + const rc = try allocator.alignedAlloc( + sig.sync.ReferenceCounter, + std.mem.page_size, + num_frames, + ); + errdefer allocator.free(rc); + @memset(rc, .{ .state = .{ .raw = 0 } }); + + const key = try allocator.alignedAlloc(FileIdFileOffset, std.mem.page_size, num_frames); + errdefer allocator.free(key); + @memset(key, .{ .file_id = FileId.fromInt(0), .file_offset = 0 }); + + const freq = try allocator.alignedAlloc(u2, std.mem.page_size, num_frames); + errdefer allocator.free(freq); + @memset(freq, 0); + + const in_queue = try allocator.alignedAlloc(InQueue, std.mem.page_size, num_frames); + errdefer allocator.free(in_queue); + @memset(in_queue, .none); + + const size = try allocator.alignedAlloc(FrameOffset, std.mem.page_size, num_frames); + errdefer allocator.free(size); + @memset(size, 0); + + return .{ + .rc = rc, + .key = key, + .freq = freq, + .in_queue = in_queue, + .size = size, + }; + } + + fn deinit(self: *FramesMetadata, allocator: std.mem.Allocator) void { + // NOTE: this check itself is racy, but should never happen + for (self.rc) |*rc| { + if (rc.isAlive()) { + @panic("BufferPool deinitialised with alive handles"); + } + } + allocator.free(self.rc); + allocator.free(self.key); + allocator.free(self.freq); + allocator.free(self.in_queue); + allocator.free(self.size); + self.* = undefined; + } + + // to be called on the eviction of a frame + // should never be called on a frame with rc>0 + // TODO: this should *all* be atomic (!) + fn resetFrame(self: FramesMetadata, index: FrameIndex) error{CannotResetAlive}!void { + if (self.rc[index].isAlive()) return error.CannotResetAlive; + self.freq[index] = 0; + self.in_queue[index] = .none; + self.size[index] = 0; + self.key[index] = FileIdFileOffset.INVALID; + } +}; + +/// This cache is S3-FIFO inspired, with some important modifications to reads +/// and eviction. This cache: +/// 1) Does not store any values itself. +/// 2) Never "forgets" any key that is inserted, meaning it stays inside the +/// cache until eviction is called. +/// 3) Maintains that main.cap and ghost.cap equal num_frames. +/// 4) Asserts that its main and ghost queues never get full. +/// 5) Does not allow duplicates, except when a ghost key is read again, leading +/// to it pushed to main with a frequency of 1. The key remains in ghost, +/// which is ignored when popped. +/// 6) Checks frame refcounts upon eviction attempt, and will push alive frame +/// indices back into the main queue (freq=1). +/// 7) Asserts that it will always return a value upon eviction. Calling +/// eviction when no free frames can be made available is illegal behaviour. +pub const HierarchicalFIFO = struct { + pub const Key = FrameIndex; + pub const Metadata = FramesMetadata; + pub const Fifo = std.fifo.LinearFifo(FrameIndex, .Slice); // TODO: atomics + + small: Fifo, + main: Fifo, // probably-alive items + ghost: Fifo, // probably-dead items + + pub fn init( + allocator: std.mem.Allocator, + small_size: u32, + num_frames: u32, + ) error{ InvalidArgument, OutOfMemory }!HierarchicalFIFO { + if (small_size > num_frames) return error.InvalidArgument; + + const small_buf = try allocator.alloc(FrameIndex, small_size); + errdefer allocator.free(small_buf); + + const main_buf = try allocator.alloc(FrameIndex, num_frames); + errdefer allocator.free(main_buf); + + const ghost_buf = try allocator.alloc(FrameIndex, num_frames); + errdefer allocator.free(ghost_buf); + + return .{ + .small = Fifo.init(small_buf), + .main = Fifo.init(main_buf), + .ghost = Fifo.init(ghost_buf), + }; + } + + pub fn deinit(self: *HierarchicalFIFO, allocator: std.mem.Allocator) void { + allocator.free(self.small.buf); + allocator.free(self.main.buf); + allocator.free(self.ghost.buf); + self.* = undefined; + } + + pub fn numFrames(self: *HierarchicalFIFO) u32 { + return @intCast(self.main.buf.len); + } + + pub fn insert( + self: *HierarchicalFIFO, + metadata: Metadata, + key: Key, + ) error{InvalidKey}!void { + if (key == INVALID_FRAME) return error.InvalidKey; + + switch (metadata.in_queue[key]) { + .main, .small => { + metadata.freq[key] +|= 1; + }, + .ghost => { + std.debug.assert(metadata.freq[key] == 0); + metadata.freq[key] = 1; + // Add key to main too - important to note that the key *still* + // exists within ghost, but from now on we'll ignore that entry. + self.main.writeItemAssumeCapacity(key); + metadata.in_queue[key] = .main; + }, + .none => { + if (self.small.writableLength() == 0) { + const popped_small = self.small.readItem().?; + + if (metadata.freq[popped_small] == 0) { + self.ghost.writeItemAssumeCapacity(popped_small); + metadata.in_queue[popped_small] = .ghost; + } else { + self.main.writeItemAssumeCapacity(popped_small); + metadata.in_queue[popped_small] = .main; + } + } + self.small.writeItemAssumeCapacity(key); + metadata.in_queue[key] = .small; + }, + } + } + + /// To be called when freelist is empty. + /// This does not return an optional, as the caller *requires* a key to be + /// evicted. Not being able to return a key means illegal internal state in + /// the BufferPool. + pub fn evict(self: *HierarchicalFIFO, metadata: Metadata) Key { + var alive_eviction_attempts: usize = 0; + + const dead_key: Key = while (true) { + var maybe_evicted: ?Key = null; + + if (maybe_evicted == null) maybe_evicted = self.evictGhost(metadata); + + // if we keep failing to evict a dead key, start alternating between + // evicting from main and small. This saves us from the rare case + // that every key in main is alive. In normal conditions, main + // should be evicted from first. + if (alive_eviction_attempts < 10 or alive_eviction_attempts % 2 == 0) { + if (maybe_evicted == null) maybe_evicted = self.evictSmallOrMain(metadata, .main); + if (maybe_evicted == null) maybe_evicted = self.evictSmallOrMain(metadata, .small); + } else { + if (maybe_evicted == null) maybe_evicted = self.evictSmallOrMain(metadata, .small); + if (maybe_evicted == null) maybe_evicted = self.evictSmallOrMain(metadata, .main); + } + + // NOTE: This panic is effectively unreachable - an empty cache + // shouldn't be possible by (mis)using the public API of BufferPool, + // except by touching the .eviction_lfu field (which you should + // never do). + const evicted = maybe_evicted orelse + @panic("unable to evict: cache empty"); // see above comment + + // alive evicted keys are reinserted, we try again + if (metadata.rc[evicted].isAlive()) { + metadata.freq[evicted] = 1; + self.main.writeItemAssumeCapacity(evicted); + metadata.in_queue[evicted] = .main; + alive_eviction_attempts += 1; + continue; + } + + // key is definitely dead + metadata.in_queue[evicted] = .none; + break evicted; + }; + + return dead_key; + } + + fn evictGhost(self: *HierarchicalFIFO, metadata: Metadata) ?Key { + const evicted: ?Key = while (self.ghost.readItem()) |ghost_key| { + switch (metadata.in_queue[ghost_key]) { + .ghost => { + break ghost_key; + }, + .main => { + // This key has moved from ghost to main, we will just pop + // and ignore it. + }, + .none => { + // This key moved from ghost to main, and then was evicted + // from main. However, because ghost is always evicted from + // before main, this should not be possible. + unreachable; + }, + .small => unreachable, + } + } else null; + + return evicted; + } + + fn evictSmallOrMain( + self: *HierarchicalFIFO, + metadata: Metadata, + comptime target_queue: enum { small, main }, + ) ?Key { + const queue = switch (target_queue) { + .small => &self.small, + .main => &self.main, + }; + + const evicted: ?Key = while (queue.readItem()) |popped_key| { + switch (target_queue) { + .small => if (metadata.in_queue[popped_key] != .small) unreachable, + .main => if (metadata.in_queue[popped_key] != .main) unreachable, + } + + if (metadata.freq[popped_key] == 0) { + break popped_key; + } else { + metadata.freq[popped_key] -|= 1; + queue.writeItemAssumeCapacity(popped_key); + } + } else null; + + return evicted; + } +}; + +/// slice-like datatype +/// view over one or more buffers owned by the BufferPool +pub const CachedRead = struct { + buffer_pool: *BufferPool, + frame_indices: []const FrameIndex, + /// inclusive, the offset into the first frame + first_frame_start_offset: FrameOffset, + /// exclusive, the offset into the last frame + last_frame_end_offset: FrameOffset, + + pub const Iterator = struct { + cached_read: *const CachedRead, + bytes_read: u32 = 0, + + const Reader = std.io.GenericReader(*Iterator, error{}, readBytes); + + pub fn next(self: *Iterator) ?u8 { + if (self.bytes_read == self.cached_read.len()) { + return null; + } + defer self.bytes_read += 1; + return self.cached_read.readByte(self.bytes_read); + } + + pub fn reset(self: *Iterator) void { + self.bytes_read = 0; + } + + pub fn readBytes(self: *Iterator, buffer: []u8) error{}!usize { + var i: u32 = 0; + while (i < buffer.len) : (i += 1) { + buffer[i] = self.next() orelse break; + } + return i; + } + + pub fn reader(self: *Iterator) Reader { + return .{ .context = self }; + } + }; + + pub fn readByte(self: CachedRead, index: usize) u8 { + std.debug.assert(self.frame_indices.len != 0); + std.debug.assert(index < self.len()); + const offset = index + self.first_frame_start_offset; + std.debug.assert(offset >= self.first_frame_start_offset); + + return self.buffer_pool.frames[ + self.frame_indices[offset / FRAME_SIZE] + ][offset % FRAME_SIZE]; + } + + /// Copies entire read into specified buffer. Must be correct length. + pub fn readAll( + self: CachedRead, + buf: []u8, + ) error{InvalidArgument}!void { + if (buf.len != self.len()) return error.InvalidArgument; + var bytes_copied: usize = 0; + + for (0.., self.frame_indices) |i, f_idx| { + const is_first_frame: u2 = @intFromBool(i == 0); + const is_last_frame: u2 = @intFromBool(i == self.frame_indices.len - 1); + + switch (is_first_frame << 1 | is_last_frame) { + 0b00 => { // !first, !last (middle frame) + const read_len = FRAME_SIZE; + @memcpy( + buf[bytes_copied..][0..read_len], + &self.buffer_pool.frames[f_idx], + ); + bytes_copied += read_len; + }, + 0b10 => { // first, !last (first frame) + std.debug.assert(i == 0); + const read_len = FRAME_SIZE - self.first_frame_start_offset; + @memcpy( + buf[0..read_len], + self.buffer_pool.frames[f_idx][self.first_frame_start_offset..], + ); + bytes_copied += read_len; + }, + 0b01 => { // !first, last (last frame) + const read_len = self.last_frame_end_offset; + @memcpy( + buf[bytes_copied..][0..read_len], + self.buffer_pool.frames[f_idx][0..read_len], + ); + bytes_copied += read_len; + std.debug.assert(bytes_copied == self.len()); + }, + 0b11 => { // first, last (only frame) + std.debug.assert(self.frame_indices.len == 1); + const readable_len = self.len(); + @memcpy( + buf[0..readable_len], + self.buffer_pool.frames[ + f_idx + ][self.first_frame_start_offset..self.last_frame_end_offset], + ); + bytes_copied += self.len(); + }, + } + } + } + + pub fn iterator(self: *const CachedRead) Iterator { + return .{ .cached_read = self }; + } + + pub fn len(self: CachedRead) u32 { + if (self.frame_indices.len == 0) return 0; + return (@as(u32, @intCast(self.frame_indices.len)) - 1) * + FRAME_SIZE + self.last_frame_end_offset - self.first_frame_start_offset; + } + + pub fn deinit(self: CachedRead, allocator: std.mem.Allocator) void { + for (self.frame_indices) |frame_index| { + std.debug.assert(frame_index != INVALID_FRAME); + + if (self.buffer_pool.frames_metadata.rc[frame_index].release()) { + // notably, the frame remains in memory, and its hashmap entry + // remains valid. + } + } + allocator.free(self.frame_indices); + } +}; + +/// Used for atomic appends + pops; No guarantees for elements. +/// Methods follow that of ArrayListUnmanaged +pub fn AtomicStack(T: type) type { + return struct { + const Self = @This(); + + buf: [*]T, + len: std.atomic.Value(usize), + cap: usize, // fixed + + fn init(allocator: std.mem.Allocator, cap: usize) !Self { + const buf = try allocator.alloc(T, cap); + return .{ + .buf = buf.ptr, + .len = .{ .raw = 0 }, + .cap = cap, + }; + } + + fn deinit(self: *Self, allocator: std.mem.Allocator) void { + allocator.free(self.buf[0..self.cap]); + self.* = undefined; + } + + // add new item to the end of buf, incrementing self.len atomically + fn appendAssumeCapacity(self: *Self, item: T) void { + const prev_len = self.len.load(.acquire); + std.debug.assert(prev_len < self.cap); + self.buf[prev_len] = item; + _ = self.len.fetchAdd(1, .release); + } + + // return item at end of buf, decrementing self.len atomically + fn popOrNull(self: *Self) ?T { + const prev_len = self.len.fetchSub(1, .acquire); + if (prev_len == 0) { + _ = self.len.fetchAdd(1, .release); + return null; + } + return self.buf[prev_len - 1]; + } + }; +} + +test AtomicStack { + const allocator = std.testing.allocator; + var stack = try AtomicStack(usize).init(allocator, 100); + defer stack.deinit(allocator); + + for (0..100) |i| stack.appendAssumeCapacity(i); + + var i: usize = 100; + while (i > 0) { + i -= 1; + try std.testing.expectEqual(i, stack.popOrNull()); + } +} + +test "BufferPool indicesRequired" { + const TestCase = struct { + start: FileOffset, + end: FileOffset, + expected: u32, + }; + const F_SIZE = FRAME_SIZE; + + const cases = [_]TestCase{ + .{ .start = 0, .end = 1, .expected = 1 }, + .{ .start = 1, .end = 1, .expected = 0 }, + .{ .start = 0, .end = F_SIZE, .expected = 1 }, + .{ .start = F_SIZE / 2, .end = (F_SIZE * 3) / 2, .expected = 2 }, + .{ .start = F_SIZE, .end = F_SIZE * 2, .expected = 1 }, + }; + + for (0.., cases) |i, case| { + errdefer std.debug.print("failed on case(i={}): {}", .{ i, case }); + try std.testing.expectEqual( + case.expected, + BufferPool.computeNumberofFrameIndices(case.start, case.end), + ); + } +} + +test "BufferPool init deinit" { + const allocator = std.testing.allocator; + + for (0.., &[_]u32{ + 2, 3, 4, 8, + 16, 32, 256, 4096, + 16384, 16385, 24576, 32767, + 32768, 49152, 65535, 65536, + }) |i, frame_count| { + errdefer std.debug.print("failed on case(i={}): {}", .{ i, frame_count }); + var bp = try BufferPool.init(allocator, frame_count); + bp.deinit(allocator); + } +} + +test "BufferPool readBlocking" { + const allocator = std.testing.allocator; + + const file = try std.fs.cwd().openFile("data/test-data/test_account_file", .{}); + defer file.close(); + const file_id = FileId.fromInt(1); + + var bp = try BufferPool.init(allocator, 2048); // 2048 frames = 1MiB + defer bp.deinit(allocator); + + var read = try bp.readBlocking(allocator, file, file_id, 0, 1000); + defer read.deinit(allocator); +} + +test "BufferPool readIoUringSubmitAndWait" { + if (!use_io_uring) return error.SkipZigTest; + + const allocator = std.testing.allocator; + + const file = try std.fs.cwd().openFile("data/test-data/test_account_file", .{}); + defer file.close(); + const file_id = FileId.fromInt(1); + + var bp = try BufferPool.init(allocator, 2048); // 2048 frames = 1MiB + defer bp.deinit(allocator); + + var read = try bp.readIoUringSubmitAndWait(allocator, file, file_id, 0, 1000); + defer read.deinit(allocator); +} + +test "BufferPool basic usage" { + const allocator = std.testing.allocator; + + const file = try std.fs.cwd().openFile("data/test-data/test_account_file", .{}); + defer file.close(); + const file_id = FileId.fromInt(1); + + var bp = try BufferPool.init(allocator, 2048); // 2048 frames = 1MiB + defer bp.deinit(allocator); + + var fba_buf: [4096]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&fba_buf); + + const read = try bp.read(fba.allocator(), file, file_id, 0, 1000); + defer read.deinit(fba.allocator()); + + try std.testing.expectEqual(2, read.frame_indices.len); + for (read.frame_indices) |f_idx| try std.testing.expect(f_idx != INVALID_FRAME); + + { + var iter1 = read.iterator(); + const reader_data = try iter1.reader().readAllAlloc(fba.allocator(), 1000); + defer fba.allocator().free(reader_data); + try std.testing.expectEqual(1000, reader_data.len); + + var iter2 = read.iterator(); + + const iter_data = try fba.allocator().alloc(u8, 1000); + defer fba.allocator().free(iter_data); + + var bytes_read: usize = 0; + while (iter2.next()) |byte| : (bytes_read += 1) { + iter_data[bytes_read] = byte; + } + try std.testing.expectEqual(1000, bytes_read); + try std.testing.expectEqualSlices(u8, reader_data, iter_data); + } +} + +test "BufferPool allocation sizes" { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .enable_memory_limit = true, + }){}; + const allocator = gpa.allocator(); + + const frame_count = 2048; // 2048 frames = 1MiB cached + + var bp = try BufferPool.init(allocator, frame_count); + defer bp.deinit(allocator); + + // We expect all allocations to be a multiple of the frame size in length + // except for the s3_fifo queues, which are split to be ~90% and ~10% of that + // length. + var total_requested_bytes = gpa.total_requested_bytes; + total_requested_bytes -= bp.eviction_lfu.ghost.buf.len * @sizeOf(FrameIndex); + total_requested_bytes -= bp.eviction_lfu.main.buf.len * @sizeOf(FrameIndex); + total_requested_bytes -= bp.eviction_lfu.small.buf.len * @sizeOf(FrameIndex); + total_requested_bytes -= @sizeOf(usize) * 3; // hashmap header + + try std.testing.expect(total_requested_bytes % frame_count == 0); + + // metadata should be small! + // As of writing, all metadata (excluding eviction_lfu, including frame_map) + // is 50 bytes or ~9% of memory usage at a frame size of 512, or 50MB for a + // million frames. + try std.testing.expect((total_requested_bytes / frame_count) - 512 <= 64); +} + +test "BufferPool filesize > frame_size * num_frames" { + const allocator = std.testing.allocator; + + const file = try std.fs.cwd().openFile("data/test-data/test_account_file", .{}); + defer file.close(); + const file_id = FileId.fromInt(1); + + const num_frames = 200; + + const file_size = (try file.stat()).size; + if (file_size < FRAME_SIZE * num_frames) @panic("file too small for valid test"); + + var bp = try BufferPool.init(std.heap.page_allocator, num_frames); + defer bp.deinit(std.heap.page_allocator); + + // can't read buffer larger than total size of the buffer pool + const read_whole = bp.read(allocator, file, file_id, 0, @intCast(file_size - 1)); + try std.testing.expectEqual(error.OffsetsOutOfBounds, read_whole); + + // file_size > total buffers size => we evict as we go + var offset: u32 = 0; + while (offset < file_size) : (offset += FRAME_SIZE) { + // when we've already filled every frame, we evict as we go + // => free list should be empty + if (offset >= FRAME_SIZE * num_frames) { + try std.testing.expectEqual(0, bp.free_list.len.raw); + } else { + try std.testing.expect(bp.free_list.len.raw > 0); + } + + const read_frame = try bp.read( + allocator, + file, + file_id, + offset, + offset + FRAME_SIZE, + ); + + try std.testing.expectEqual(1, read_frame.frame_indices.len); + + const frame: []const u8 = bp.frames[ + read_frame.frame_indices[0] + ][0..bp.frames_metadata.size[ + read_frame.frame_indices[0] + ]]; + + var frame2: [FRAME_SIZE]u8 = undefined; + const bytes_read = try file.preadAll(&frame2, offset); + try std.testing.expectEqualSlices(u8, frame2[0..bytes_read], frame); + read_frame.deinit(allocator); + } +} + +test "BufferPool random read" { + const allocator = std.testing.allocator; + + const file = try std.fs.cwd().openFile("data/test-data/test_account_file", .{}); + defer file.close(); + const file_id = FileId.fromInt(1); + + const num_frames = 200; + + var bp = try BufferPool.init(allocator, num_frames); + defer bp.deinit(allocator); + + const file_size: u32 = @intCast((try file.stat()).size); + + var prng = std.Random.DefaultPrng.init(5083); + + var reads: usize = 0; + while (reads < 5000) : (reads += 1) { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .safety = true, + }){}; + defer _ = gpa.deinit(); + + const range_start = prng.random().intRangeAtMost(u32, 0, file_size); + const range_end = prng.random().intRangeAtMost( + u32, + range_start, + @min(file_size, range_start + num_frames * FRAME_SIZE), + ); + + if (try BufferPool.computeNumberofFrameIndices(range_start, range_end) > num_frames) { + continue; + } + + var read = try bp.read(gpa.allocator(), file, file_id, range_start, range_end); + defer read.deinit(gpa.allocator()); + + // check for equality with other impl + if (use_io_uring) { + var read2 = try bp.readBlocking( + gpa.allocator(), + file, + file_id, + range_start, + range_end, + ); + defer read2.deinit(gpa.allocator()); + + try std.testing.expect( + read.first_frame_start_offset == read2.first_frame_start_offset, + ); + try std.testing.expect(read.last_frame_end_offset == read2.last_frame_end_offset); + try std.testing.expectEqualSlices(u32, read.frame_indices, read2.frame_indices); + } + + var total_bytes_read: u32 = 0; + for (read.frame_indices) |f_idx| total_bytes_read += bp.frames_metadata.size[f_idx]; + const read_data_bp_iter = try allocator.alloc(u8, read.len()); + defer allocator.free(read_data_bp_iter); + { + var i: u32 = 0; + var iter = read.iterator(); + while (iter.next()) |b| : (i += 1) read_data_bp_iter[i] = b; + if (i != read.len()) unreachable; + } + + var iter = read.iterator(); + const read_data_bp_reader = try iter.reader().readAllAlloc( + allocator, + num_frames * FRAME_SIZE, + ); + defer allocator.free(read_data_bp_reader); + + const read_data_bp_readall = try allocator.alloc(u8, read.len()); + defer allocator.free(read_data_bp_readall); + try read.readAll(read_data_bp_readall); + + const read_data_expected = try allocator.alloc(u8, range_end - range_start); + defer allocator.free(read_data_expected); + const preaded_bytes = try file.preadAll(read_data_expected, range_start); + + // read via iterator + try std.testing.expectEqualSlices(u8, read_data_expected, read_data_bp_iter); + try std.testing.expectEqual(preaded_bytes, read_data_bp_iter.len); + + // read via reader + try std.testing.expectEqualSlices(u8, read_data_expected, read_data_bp_reader); + try std.testing.expectEqual(preaded_bytes, read_data_bp_reader.len); + + // read via .readAll() + try std.testing.expectEqualSlices(u8, read_data_expected, read_data_bp_readall); + try std.testing.expectEqual(preaded_bytes, read_data_bp_readall.len); + } +} diff --git a/src/accountsdb/db.zig b/src/accountsdb/db.zig index 398bfe86c..2bb21669d 100644 --- a/src/accountsdb/db.zig +++ b/src/accountsdb/db.zig @@ -22,13 +22,13 @@ const AccountInFile = sig.accounts_db.accounts_file.AccountInFile; const FileId = sig.accounts_db.accounts_file.FileId; const AccountsDbFields = sig.accounts_db.snapshots.AccountsDbFields; -const AllSnapshotFields = sig.accounts_db.snapshots.AllSnapshotFields; +const FullAndIncrementalManifest = sig.accounts_db.snapshots.FullAndIncrementalManifest; const BankFields = sig.accounts_db.snapshots.BankFields; const BankHashStats = sig.accounts_db.snapshots.BankHashStats; const BankIncrementalSnapshotPersistence = sig.accounts_db.snapshots.BankIncrementalSnapshotPersistence; const FullSnapshotFileInfo = sig.accounts_db.snapshots.FullSnapshotFileInfo; const IncrementalSnapshotFileInfo = sig.accounts_db.snapshots.IncrementalSnapshotFileInfo; -const SnapshotFields = sig.accounts_db.snapshots.SnapshotFields; +const SnapshotManifest = sig.accounts_db.snapshots.Manifest; const SnapshotFiles = sig.accounts_db.snapshots.SnapshotFiles; const AccountIndex = sig.accounts_db.index.AccountIndex; @@ -43,7 +43,6 @@ const Slot = sig.core.Slot; const NestedHashTree = sig.utils.merkle_tree.NestedHashTree; const Logger = sig.trace.log.Logger; - const GeyserWriter = sig.geyser.GeyserWriter; const Counter = sig.prometheus.counter.Counter; @@ -60,6 +59,9 @@ const spawnThreadTasks = sig.utils.thread.spawnThreadTasks; const printTimeEstimate = sig.time.estimate.printTimeEstimate; const globalRegistry = sig.prometheus.registry.globalRegistry; +const LOG_SCOPE = "accounts_db"; +const ScopedLogger = sig.trace.log.ScopedLogger(LOG_SCOPE); + pub const DB_LOG_RATE = sig.time.Duration.fromSecs(5); pub const DB_MANAGER_LOOP_MIN = sig.time.Duration.fromSecs(5); @@ -74,10 +76,10 @@ pub const DELETE_ACCOUNT_FILES_MIN = 100; pub const AccountsDB = struct { // injected dependencies - allocator: std.mem.Allocator, metrics: AccountsDBMetrics, - logger: Logger, + logger: ScopedLogger, + /// Not closed by the `AccountsDB`, but must live at least as long as it. snapshot_dir: std.fs.Dir, geyser_writer: ?*GeyserWriter, @@ -204,7 +206,7 @@ pub const AccountsDB = struct { return .{ .allocator = params.allocator, .metrics = metrics, - .logger = params.logger, + .logger = params.logger.withScope(LOG_SCOPE), .snapshot_dir = params.snapshot_dir, .geyser_writer = params.geyser_writer, .gossip_view = params.gossip_view, @@ -275,25 +277,26 @@ pub const AccountsDB = struct { /// needs to be a thread-safe allocator allocator: std.mem.Allocator, /// Must have been allocated with `self.allocator`. - all_snapshot_fields: *AllSnapshotFields, + full_inc_manifest: FullAndIncrementalManifest, n_threads: u32, validate: bool, accounts_per_file_estimate: u64, should_fastload: bool, save_index: bool, - ) !SnapshotFields { - const snapshot_fields = try all_snapshot_fields.collapse(self.allocator); + ) !SnapshotManifest { + const collapsed_manifest = try full_inc_manifest.collapse(self.allocator); + errdefer collapsed_manifest.deinit(self.allocator); if (should_fastload) { var timer = try sig.time.Timer.start(); var fastload_dir = try self.snapshot_dir.makeOpenPath("fastload_state", .{}); defer fastload_dir.close(); self.logger.info().log("fast loading accountsdb..."); - try self.fastload(fastload_dir, snapshot_fields.accounts_db_fields); + try self.fastload(fastload_dir, collapsed_manifest.accounts_db_fields); self.logger.info().logf("loaded from snapshot in {s}", .{timer.read()}); } else { const load_duration = try self.loadFromSnapshot( - snapshot_fields.accounts_db_fields, + collapsed_manifest.accounts_db_fields, n_threads, allocator, accounts_per_file_estimate, @@ -306,20 +309,24 @@ pub const AccountsDB = struct { var fastload_dir = try self.snapshot_dir.makeOpenPath("fastload_state", .{}); defer fastload_dir.close(); - self.logger.info().log("saving account index state to disk..."); - try self.account_index.saveToDisk(fastload_dir, self.logger); + try self.account_index.saveToDisk(fastload_dir); } if (validate) { - const full_snapshot = all_snapshot_fields.full; + const full_man = full_inc_manifest.full; + const maybe_inc_persistence = if (full_inc_manifest.incremental) |inc| + inc.bank_extra.snapshot_persistence + else + null; + var validate_timer = try sig.time.Timer.start(); try self.validateLoadFromSnapshot(.{ - .full_slot = full_snapshot.bank_fields.slot, + .full_slot = full_man.bank_fields.slot, .expected_full = .{ - .accounts_hash = snapshot_fields.accounts_db_fields.bank_hash_info.accounts_hash, - .capitalization = full_snapshot.bank_fields.capitalization, + .accounts_hash = full_man.accounts_db_fields.bank_hash_info.accounts_hash, + .capitalization = full_man.bank_fields.capitalization, }, - .expected_incremental = if (snapshot_fields.bank_extra.snapshot_persistence) |inc_persistence| .{ + .expected_incremental = if (maybe_inc_persistence) |inc_persistence| .{ .accounts_hash = inc_persistence.incremental_hash, .capitalization = inc_persistence.incremental_capitalization, } else null, @@ -327,7 +334,7 @@ pub const AccountsDB = struct { self.logger.info().logf("validated from snapshot in {s}", .{validate_timer.read()}); } - return snapshot_fields; + return collapsed_manifest; } pub fn fastload( @@ -377,7 +384,7 @@ pub const AccountsDB = struct { // NOTE: index loading was the most expensive part which we fastload here self.logger.info().log("loading account index"); - try self.account_index.loadFromDisk(dir, self.logger); + try self.account_index.loadFromDisk(dir); } /// loads the account files and generates the account index from a snapshot @@ -704,7 +711,7 @@ pub const AccountsDB = struct { if (print_progress and progress_timer.read().asNanos() > DB_LOG_RATE.asNanos()) { printTimeEstimate( - self.logger.withScope(@typeName(Self)), + self.logger, &timer, n_account_files, file_count, @@ -742,7 +749,7 @@ pub const AccountsDB = struct { if (print_progress and progress_timer.read().asNanos() > DB_LOG_RATE.asNanos()) { printTimeEstimate( - self.logger.withScope(@typeName(Self)), + self.logger, &timer, n_accounts_total, ref_count, @@ -761,14 +768,14 @@ pub const AccountsDB = struct { thread_dbs: []AccountsDB, n_threads: usize, ) !void { - var combine_indexes_wg: std.Thread.WaitGroup = .{}; - defer combine_indexes_wg.wait(); - try spawnThreadTasks(combineThreadIndexesMultiThread, .{ - .wg = &combine_indexes_wg, + var merge_indexes_wg: std.Thread.WaitGroup = .{}; + defer merge_indexes_wg.wait(); + try spawnThreadTasks(mergeThreadIndexesMultiThread, .{ + .wg = &merge_indexes_wg, .data_len = self.account_index.pubkey_ref_map.numberOfShards(), .max_threads = n_threads, .params = .{ - self.logger.unscoped(), + self.logger, &self.account_index, thread_dbs, }, @@ -830,13 +837,12 @@ pub const AccountsDB = struct { /// combines multiple thread indexes into the given index. /// each bin is also sorted by pubkey. - pub fn combineThreadIndexesMultiThread( - logger_: Logger, + pub fn mergeThreadIndexesMultiThread( + logger: ScopedLogger, index: *AccountIndex, thread_dbs: []const AccountsDB, task: sig.utils.thread.TaskParams, ) !void { - const logger = logger_.withScope(@typeName(Self)); const shard_start_index = task.start_index; const shard_end_index = task.end_index; @@ -880,7 +886,7 @@ pub const AccountsDB = struct { &timer, total_shards, iteration_count, - "combining thread indexes", + "merging thread indexes", "thread0", ); progress_timer.reset(); @@ -1239,7 +1245,7 @@ pub const AccountsDB = struct { if (print_progress and progress_timer.read() > DB_LOG_RATE.asNanos()) { printTimeEstimate( - self.logger.withScope(@typeName(Self)), + self.logger, &timer, shards.len, count, @@ -1257,7 +1263,6 @@ pub const AccountsDB = struct { pub fn createAccountFile(self: *Self, size: usize, slot: Slot) !struct { std.fs.File, FileId, - []u8, } { self.largest_file_id = self.largest_file_id.increment(); const file_id = self.largest_file_id; @@ -1274,16 +1279,7 @@ pub const AccountsDB = struct { try file.seekTo(0); } - const memory = try std.posix.mmap( - null, - size, - std.posix.PROT.READ | std.posix.PROT.WRITE, - std.posix.MAP{ .TYPE = .SHARED }, - file.handle, - 0, - ); - - return .{ file, file_id, memory }; + return .{ file, file_id }; } pub const ManagerLoopConfig = struct { @@ -1500,17 +1496,29 @@ pub const AccountsDB = struct { self.metrics.flush_account_file_size.observe(account_size_in_file); } - const file, const file_id, const memory = try self.createAccountFile(size, slot); + const file, const file_id = try self.createAccountFile(size, slot); defer file.close(); const offsets = try self.allocator.alloc(u64, accounts.len); defer self.allocator.free(offsets); + var file_size: usize = 0; + for (accounts) |account| file_size += account.getSizeInFile(); + + var account_file_buf = std.ArrayList(u8).init(self.allocator); + defer account_file_buf.deinit(); + var current_offset: u64 = 0; for (offsets, accounts, pubkeys) |*offset, account, pubkey| { + try account_file_buf.resize(account.getSizeInFile()); + offset.* = current_offset; // write the account to the file - current_offset += account.writeToBuf(&pubkey, memory[current_offset..]); + const bytes_written = account.writeToBuf(&pubkey, account_file_buf.items); + current_offset += bytes_written; + + if (bytes_written != account.getSizeInFile()) unreachable; + try file.writeAll(account_file_buf.items); } var account_file = try AccountFile.init(file, .{ @@ -1899,12 +1907,25 @@ pub const AccountsDB = struct { self.metrics.shrink_file_shrunk_by.observe(accounts_dead_size); // alloc account file for accounts - const new_file, const new_file_id, const new_memory = try self.createAccountFile( + const new_file, const new_file_id = try self.createAccountFile( accounts_alive_size, slot, ); defer new_file.close(); + var file_size: usize = 0; + account_iter.reset(); + for (is_alive_flags.items) |is_alive| { + // SAFE: we know is_alive_flags is the same length as the account_iter + const account = account_iter.next().?; + if (is_alive) { + file_size += account.getSizeInFile(); + } + } + + var account_file_buf = std.ArrayList(u8).init(self.allocator); + defer account_file_buf.deinit(); + // write the alive accounts var offsets = try std.ArrayList(u64).initCapacity(self.allocator, accounts_alive_count); defer offsets.deinit(); @@ -1915,8 +1936,10 @@ pub const AccountsDB = struct { // SAFE: we know is_alive_flags is the same length as the account_iter const account = account_iter.next().?; if (is_alive) { + try account_file_buf.resize(account.getSizeInFile()); offsets.appendAssumeCapacity(offset); - offset += account.writeToBuf(new_memory[offset..]); + offset += account.writeToBuf(account_file_buf.items); + try new_file.writeAll(account_file_buf.items); } } @@ -2655,7 +2678,7 @@ pub const AccountsDB = struct { params.bank_fields.slot = params.target_slot; // ! params.bank_fields.capitalization = full_capitalization; // ! - const snapshot_fields: SnapshotFields = .{ + const manifest: SnapshotManifest = .{ .bank_fields = params.bank_fields.*, .accounts_db_fields = .{ .file_map = serializable_file_map, @@ -2686,7 +2709,7 @@ pub const AccountsDB = struct { zstd_write_ctx.writer(), sig.version.CURRENT_CLIENT_VERSION, StatusCache.EMPTY, - &snapshot_fields, + &manifest, file_map, ); try zstd_write_ctx.finish(); @@ -2885,7 +2908,7 @@ pub const AccountsDB = struct { params.bank_fields.slot = params.target_slot; // ! - const snapshot_fields: SnapshotFields = .{ + const manifest: SnapshotManifest = .{ .bank_fields = params.bank_fields.*, .accounts_db_fields = .{ .file_map = serializable_file_map, @@ -2916,7 +2939,7 @@ pub const AccountsDB = struct { zstd_write_ctx.writer(), sig.version.CURRENT_CLIENT_VERSION, StatusCache.EMPTY, - &snapshot_fields, + &manifest, file_map, ); try zstd_write_ctx.finish(); @@ -3176,13 +3199,13 @@ pub fn indexAndValidateAccountFile( accounts_file.number_of_accounts = number_of_accounts; } -/// All entries in `snapshot_fields.accounts_db_fields.file_map` must correspond to an entry in `file_map`, +/// All entries in `manifest.accounts_db_fields.file_map` must correspond to an entry in `file_map`, /// with the association defined by the file id (a field of the value of the former, the key of the latter). pub fn writeSnapshotTarWithFields( archive_writer: anytype, version: sig.version.ClientVersion, status_cache: StatusCache, - manifest: *const SnapshotFields, + manifest: *const SnapshotManifest, file_map: *const AccountsDB.FileMap, ) !void { var counting_state = if (std.debug.runtime_safety) std.io.countingWriter(archive_writer); @@ -3220,7 +3243,7 @@ fn testWriteSnapshotFull( const manifest_file = try snapshot_dir.openFile(manifest_path_bounded.constSlice(), .{}); defer manifest_file.close(); - var snap_fields = try SnapshotFields.decodeFromBincode(allocator, manifest_file.reader()); + var snap_fields = try SnapshotManifest.decodeFromBincode(allocator, manifest_file.reader()); defer snap_fields.deinit(allocator); _ = try accounts_db.loadFromSnapshot(snap_fields.accounts_db_fields, 1, allocator, 1_500); @@ -3259,7 +3282,7 @@ fn testWriteSnapshotIncremental( const manifest_file = try snapshot_dir.openFile(manifest_path_bounded.constSlice(), .{}); defer manifest_file.close(); - var snap_fields = try SnapshotFields.decodeFromBincode(allocator, manifest_file.reader()); + var snap_fields = try SnapshotManifest.decodeFromBincode(allocator, manifest_file.reader()); defer snap_fields.deinit(allocator); _ = try accounts_db.loadFromSnapshot(snap_fields.accounts_db_fields, 1, allocator, 1_500); @@ -3304,26 +3327,26 @@ test "testWriteSnapshot" { const snap_files = try SnapshotFiles.find(allocator, test_data_dir); - var tmp_snap_dir_root = std.testing.tmpDir(.{}); - defer tmp_snap_dir_root.cleanup(); - const tmp_snap_dir = tmp_snap_dir_root.dir; + var tmp_dir_root = std.testing.tmpDir(.{}); + defer tmp_dir_root.cleanup(); + const snapshot_dir = tmp_dir_root.dir; { const archive_file = try test_data_dir.openFile(snap_files.full_snapshot.snapshotArchiveName().constSlice(), .{}); defer archive_file.close(); - try parallelUnpackZstdTarBall(allocator, .noop, archive_file, tmp_snap_dir, 4, true); + try parallelUnpackZstdTarBall(allocator, .noop, archive_file, snapshot_dir, 4, true); } if (snap_files.incremental()) |inc_snap| { const archive_file = try test_data_dir.openFile(inc_snap.snapshotArchiveName().constSlice(), .{}); defer archive_file.close(); - try parallelUnpackZstdTarBall(allocator, .noop, archive_file, tmp_snap_dir, 4, false); + try parallelUnpackZstdTarBall(allocator, .noop, archive_file, snapshot_dir, 4, false); } var accounts_db = try AccountsDB.init(.{ .allocator = allocator, .logger = .noop, - .snapshot_dir = tmp_snap_dir, + .snapshot_dir = snapshot_dir, .geyser_writer = null, .gossip_view = null, .index_allocation = .ram, @@ -3403,7 +3426,7 @@ fn loadTestAccountsDB( /// The directory into which the snapshots are unpacked, and /// the `snapshots_dir` for the returned `AccountsDB`. snapshot_dir: std.fs.Dir, -) !struct { AccountsDB, AllSnapshotFields } { +) !struct { AccountsDB, FullAndIncrementalManifest } { comptime std.debug.assert(builtin.is_test); // should only be used in tests var dir = try std.fs.cwd().openDir(sig.TEST_DATA_DIR, .{ .iterate = true }); @@ -3411,10 +3434,12 @@ fn loadTestAccountsDB( const snapshot_files = try findAndUnpackTestSnapshots(n_threads, snapshot_dir); - var snapshots = try AllSnapshotFields.fromFiles(allocator, logger, snapshot_dir, snapshot_files); - errdefer snapshots.deinit(allocator); + const full_inc_manifest = + try FullAndIncrementalManifest.fromFiles(allocator, logger, snapshot_dir, snapshot_files); + errdefer full_inc_manifest.deinit(allocator); - const snapshot = try snapshots.collapse(allocator); + const manifest = try full_inc_manifest.collapse(allocator); + defer manifest.deinit(allocator); var accounts_db = try AccountsDB.init(.{ .allocator = allocator, @@ -3429,13 +3454,13 @@ fn loadTestAccountsDB( errdefer accounts_db.deinit(); _ = try accounts_db.loadFromSnapshot( - snapshot.accounts_db_fields, + manifest.accounts_db_fields, n_threads, allocator, 500, ); - return .{ accounts_db, snapshots }; + return .{ accounts_db, full_inc_manifest }; } // NOTE: this is a memory leak test - geyser correctness is tested in the geyser tests @@ -3445,11 +3470,13 @@ test "geyser stream on load" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; - const snapshot_files = try findAndUnpackTestSnapshots(2, snapdir); + const snapshot_dir = tmp_dir_root.dir; + + const snapshot_files = try findAndUnpackTestSnapshots(2, snapshot_dir); - var snapshots = try AllSnapshotFields.fromFiles(allocator, logger, snapdir, snapshot_files); - errdefer snapshots.deinit(allocator); + const full_inc_manifest = + try FullAndIncrementalManifest.fromFiles(allocator, logger, snapshot_dir, snapshot_files); + defer full_inc_manifest.deinit(allocator); var geyser_exit = std.atomic.Value(bool).init(false); @@ -3480,13 +3507,13 @@ test "geyser stream on load" { defer geyser_exit.store(true, .release); - const snapshot = try snapshots.collapse(allocator); - defer snapshots.deinit(allocator); + const snapshot = try full_inc_manifest.collapse(allocator); + defer snapshot.deinit(allocator); var accounts_db = try AccountsDB.init(.{ .allocator = allocator, .logger = logger, - .snapshot_dir = snapdir, + .snapshot_dir = snapshot_dir, .geyser_writer = geyser_writer, .gossip_view = null, .index_allocation = .ram, @@ -3508,13 +3535,12 @@ test "write and read an account" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 1, .noop, snapdir); - defer { - accounts_db.deinit(); - snapshots.deinit(allocator); - } + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 1, .noop, snapshot_dir); + defer accounts_db.deinit(); + defer full_inc_manifest.deinit(allocator); var prng = std.rand.DefaultPrng.init(0); const pubkey = Pubkey.initRandom(prng.random()); @@ -3549,21 +3575,22 @@ test "load and validate from test snapshot" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 1, .noop, snapdir); + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 1, .noop, snapshot_dir); defer { accounts_db.deinit(); - snapshots.deinit(allocator); + full_inc_manifest.deinit(allocator); } try accounts_db.validateLoadFromSnapshot(.{ - .full_slot = snapshots.full.bank_fields.slot, + .full_slot = full_inc_manifest.full.bank_fields.slot, .expected_full = .{ - .accounts_hash = snapshots.full.accounts_db_fields.bank_hash_info.accounts_hash, - .capitalization = snapshots.full.bank_fields.capitalization, + .accounts_hash = full_inc_manifest.full.accounts_db_fields.bank_hash_info.accounts_hash, + .capitalization = full_inc_manifest.full.bank_fields.capitalization, }, - .expected_incremental = if (snapshots.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ + .expected_incremental = if (full_inc_manifest.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ .accounts_hash = inc_persistence.incremental_hash, .capitalization = inc_persistence.incremental_capitalization, } else null, @@ -3575,21 +3602,22 @@ test "load and validate from test snapshot using disk index" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 1, .noop, snapdir); + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 1, .noop, snapshot_dir); defer { accounts_db.deinit(); - snapshots.deinit(allocator); + full_inc_manifest.deinit(allocator); } try accounts_db.validateLoadFromSnapshot(.{ - .full_slot = snapshots.full.bank_fields.slot, + .full_slot = full_inc_manifest.full.bank_fields.slot, .expected_full = .{ - .accounts_hash = snapshots.full.accounts_db_fields.bank_hash_info.accounts_hash, - .capitalization = snapshots.full.bank_fields.capitalization, + .accounts_hash = full_inc_manifest.full.accounts_db_fields.bank_hash_info.accounts_hash, + .capitalization = full_inc_manifest.full.bank_fields.capitalization, }, - .expected_incremental = if (snapshots.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ + .expected_incremental = if (full_inc_manifest.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ .accounts_hash = inc_persistence.incremental_hash, .capitalization = inc_persistence.incremental_capitalization, } else null, @@ -3601,21 +3629,22 @@ test "load and validate from test snapshot parallel" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 2, .noop, snapdir); + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 2, .noop, snapshot_dir); defer { accounts_db.deinit(); - snapshots.deinit(allocator); + full_inc_manifest.deinit(allocator); } try accounts_db.validateLoadFromSnapshot(.{ - .full_slot = snapshots.full.bank_fields.slot, + .full_slot = full_inc_manifest.full.bank_fields.slot, .expected_full = .{ - .accounts_hash = snapshots.full.accounts_db_fields.bank_hash_info.accounts_hash, - .capitalization = snapshots.full.bank_fields.capitalization, + .accounts_hash = full_inc_manifest.full.accounts_db_fields.bank_hash_info.accounts_hash, + .capitalization = full_inc_manifest.full.bank_fields.capitalization, }, - .expected_incremental = if (snapshots.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ + .expected_incremental = if (full_inc_manifest.incremental.?.bank_extra.snapshot_persistence) |inc_persistence| .{ .accounts_hash = inc_persistence.incremental_hash, .capitalization = inc_persistence.incremental_capitalization, } else null, @@ -3627,16 +3656,17 @@ test "load clock sysvar" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 1, .noop, snapdir); + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 1, .noop, snapshot_dir); defer { accounts_db.deinit(); - snapshots.deinit(allocator); + full_inc_manifest.deinit(allocator); } - const full = snapshots.full; - const inc = snapshots.incremental; + const full = full_inc_manifest.full; + const inc = full_inc_manifest.incremental; const expected_clock: sysvars.Clock = .{ .slot = (inc orelse full).bank_fields.slot, .epoch_start_timestamp = 1733349736, @@ -3660,12 +3690,13 @@ test "load other sysvars" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; + const snapshot_dir = tmp_dir_root.dir; - var accounts_db, var snapshots = try loadTestAccountsDB(allocator, false, 1, .noop, snapdir); + var accounts_db, const full_inc_manifest = + try loadTestAccountsDB(allocator, false, 1, .noop, snapshot_dir); defer { accounts_db.deinit(); - snapshots.deinit(allocator); + full_inc_manifest.deinit(allocator); } const SlotAndHash = sig.accounts_db.snapshots.SlotAndHash; @@ -4241,16 +4272,13 @@ test "generate snapshot & update gossip snapshot hashes" { var tmp_dir_root = std.testing.tmpDir(.{}); defer tmp_dir_root.cleanup(); - const snapdir = tmp_dir_root.dir; - const snap_files = try findAndUnpackTestSnapshots(1, snapdir); + const snapshot_dir = tmp_dir_root.dir; - var all_snapshot_fields = try AllSnapshotFields.fromFiles( - allocator, - .noop, - snapdir, - snap_files, - ); - defer all_snapshot_fields.deinit(allocator); + const snap_files = try findAndUnpackTestSnapshots(1, snapshot_dir); + + const full_inc_manifest = + try FullAndIncrementalManifest.fromFiles(allocator, .noop, snapshot_dir, snap_files); + defer full_inc_manifest.deinit(allocator); // mock gossip service const Queue = std.ArrayList(sig.gossip.GossipData); @@ -4261,7 +4289,7 @@ test "generate snapshot & update gossip snapshot hashes" { var accounts_db = try AccountsDB.init(.{ .allocator = allocator, .logger = .noop, - .snapshot_dir = snapdir, + .snapshot_dir = snapshot_dir, .gossip_view = .{ .my_pubkey = Pubkey.fromPublicKey(&my_keypair.public_key), .push_msg_queue = &push_msg_queue_mux, @@ -4273,25 +4301,37 @@ test "generate snapshot & update gossip snapshot hashes" { }); defer accounts_db.deinit(); - // pretend `all_snapshot_fields`/`snap_files` refers to `tmp_snap_dir`, even though the archive file isn't actually in there, just the unpacked contents. - // TODO: this is not nice, make sure the API for loading from archives outside of the snapshot dir is improved. - _ = try accounts_db.loadWithDefaults(allocator, &all_snapshot_fields, 1, true, 300, false, false); + (try accounts_db.loadWithDefaults( + allocator, + full_inc_manifest, + 1, + true, + 300, + false, + false, + )).deinit(allocator); var bank_fields = try BankFields.initRandom(allocator, random, 128); defer bank_fields.deinit(allocator); - const full_slot = all_snapshot_fields.full.accounts_db_fields.slot; + const full_slot = full_inc_manifest.full.accounts_db_fields.slot; const full_gen_result = try accounts_db.generateFullSnapshot(.{ .target_slot = full_slot, .bank_fields = &bank_fields, .lamports_per_signature = random.int(u64), .old_snapshot_action = .ignore_old, // make sure we don't delete anything in `sig.TEST_DATA_DIR` - .deprecated_stored_meta_write_version = all_snapshot_fields.full.accounts_db_fields.stored_meta_write_version, + .deprecated_stored_meta_write_version = full_inc_manifest.full.accounts_db_fields.stored_meta_write_version, }); const full_hash = full_gen_result.hash; - try std.testing.expectEqual(all_snapshot_fields.full.accounts_db_fields.bank_hash_info.accounts_hash, full_gen_result.hash); - try std.testing.expectEqual(all_snapshot_fields.full.bank_fields.capitalization, full_gen_result.capitalization); + try std.testing.expectEqual( + full_inc_manifest.full.accounts_db_fields.bank_hash_info.accounts_hash, + full_gen_result.hash, + ); + try std.testing.expectEqual( + full_inc_manifest.full.bank_fields.capitalization, + full_gen_result.capitalization, + ); { const queue, var queue_lg = push_msg_queue_mux.readWithLock(); @@ -4312,18 +4352,19 @@ test "generate snapshot & update gossip snapshot hashes" { ); } - if (all_snapshot_fields.incremental) |inc_snapshot_fields| { - const inc_slot = inc_snapshot_fields.accounts_db_fields.slot; + if (full_inc_manifest.incremental) |inc_manifest| { + const inc_slot = inc_manifest.accounts_db_fields.slot; const inc_gen_result = try accounts_db.generateIncrementalSnapshot(.{ .target_slot = inc_slot, .bank_fields = &bank_fields, .lamports_per_signature = random.int(u64), .old_snapshot_action = .ignore_old, // make sure we don't delete anything in `sig.TEST_DATA_DIR` - .deprecated_stored_meta_write_version = all_snapshot_fields.incremental.?.accounts_db_fields.stored_meta_write_version, + .deprecated_stored_meta_write_version = inc_manifest + .accounts_db_fields.stored_meta_write_version, }); const inc_hash = inc_gen_result.incremental_hash; - try std.testing.expectEqual(inc_snapshot_fields.bank_extra.snapshot_persistence, inc_gen_result); + try std.testing.expectEqual(inc_manifest.bank_extra.snapshot_persistence, inc_gen_result); try std.testing.expectEqual(full_slot, inc_gen_result.full_slot); try std.testing.expectEqual(full_gen_result.hash, inc_gen_result.full_hash); try std.testing.expectEqual(full_gen_result.capitalization, inc_gen_result.full_capitalization); @@ -4349,27 +4390,36 @@ test "generate snapshot & update gossip snapshot hashes" { } } +pub fn getAccountPerFileEstimateFromCluster( + cluster: sig.core.Cluster, +) error{NotImplementedYet}!u64 { + return switch (cluster) { + .testnet => 500, + else => error.NotImplementedYet, + }; +} + pub const BenchmarkAccountsDBSnapshotLoad = struct { pub const min_iterations = 1; pub const max_iterations = 1; + pub const SNAPSHOT_DIR_PATH = sig.TEST_DATA_DIR ++ "bench_snapshot/"; + pub const BenchArgs = struct { use_disk: bool, n_threads: u32, name: []const u8, + cluster: sig.core.Cluster, + // TODO: support fastloading checks }; pub const args = [_]BenchArgs{ BenchArgs{ - .name = "RAM index (2 threads)", + .name = "testnet - ram index - 4 threads", .use_disk = false, - .n_threads = 2, + .n_threads = 4, + .cluster = .testnet, }, - // BenchArgs{ - // .use_disk = true, - // .n_threads = 2, - // .name = "DISK index (2 threads)", - // }, }; pub fn loadAndVerifySnapshot(units: BenchTimeUnit, bench_args: BenchArgs) !struct { @@ -4377,15 +4427,16 @@ pub const BenchmarkAccountsDBSnapshotLoad = struct { validate_time: u64, } { const allocator = std.heap.c_allocator; - const logger = .noop; + var print_logger = sig.trace.DirectPrintLogger.init(allocator, .debug); + const logger = print_logger.logger(); // unpack the snapshot - // NOTE: usually this will be an incremental snapshot - // renamed as a full snapshot (mv {inc-snap-fmt}.tar.zstd {full-snap-fmt}.tar.zstd) - // (because test snapshots are too small and full snapshots are too big) - const dir_path = sig.TEST_DATA_DIR ++ "bench_snapshot/"; - var snapshot_dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch { - std.debug.print("need to setup a snapshot in {s} for this benchmark...\n", .{dir_path}); + var snapshot_dir = std.fs.cwd().openDir(SNAPSHOT_DIR_PATH, .{ .iterate = true }) catch { + // not snapshot -> early exit + std.debug.print( + "need to setup a snapshot in {s} for this benchmark...\n", + .{SNAPSHOT_DIR_PATH}, + ); const zero_duration = sig.time.Duration.fromNanos(0); return .{ .load_time = zero_duration.asNanos(), @@ -4395,31 +4446,14 @@ pub const BenchmarkAccountsDBSnapshotLoad = struct { defer snapshot_dir.close(); const snapshot_files = try SnapshotFiles.find(allocator, snapshot_dir); - - var accounts_dir = inline for (0..2) |attempt| { - if (snapshot_dir.openDir("accounts", .{ .iterate = true })) |accounts_dir| - break accounts_dir - else |err| switch (err) { - else => |e| return e, - error.FileNotFound => if (attempt == 0) { - const archive_file = try snapshot_dir.openFile(snapshot_files.full.snapshotArchiveName().constSlice(), .{}); - defer archive_file.close(); - try parallelUnpackZstdTarBall( - allocator, - logger, - archive_file, - snapshot_dir, - try std.Thread.getCpuCount() / 2, - true, - ); - }, - } - } else return error.SnapshotMissingAccountsDir; - defer accounts_dir.close(); - - var snapshots = try AllSnapshotFields.fromFiles(allocator, logger, snapshot_dir, snapshot_files); - defer snapshots.deinit(allocator); - const snapshot = try snapshots.collapse(allocator); + const full_inc_manifest = try FullAndIncrementalManifest.fromFiles( + allocator, + logger, + snapshot_dir, + snapshot_files, + ); + defer full_inc_manifest.deinit(allocator); + const collapsed_manifest = try full_inc_manifest.collapse(allocator); var accounts_db = try AccountsDB.init(.{ .allocator = allocator, @@ -4434,21 +4468,21 @@ pub const BenchmarkAccountsDBSnapshotLoad = struct { defer accounts_db.deinit(); const loading_duration = try accounts_db.loadFromSnapshot( - snapshot.accounts_db_fields, + collapsed_manifest.accounts_db_fields, bench_args.n_threads, allocator, - 500, + try getAccountPerFileEstimateFromCluster(bench_args.cluster), ); - const full_snapshot = snapshots.full; + const full_snapshot = full_inc_manifest.full; var validate_timer = try sig.time.Timer.start(); try accounts_db.validateLoadFromSnapshot(.{ .full_slot = full_snapshot.bank_fields.slot, .expected_full = .{ - .accounts_hash = snapshot.accounts_db_fields.bank_hash_info.accounts_hash, + .accounts_hash = collapsed_manifest.accounts_db_fields.bank_hash_info.accounts_hash, .capitalization = full_snapshot.bank_fields.capitalization, }, - .expected_incremental = if (snapshot.bank_extra.snapshot_persistence) |inc_persistence| .{ + .expected_incremental = if (collapsed_manifest.bank_extra.snapshot_persistence) |inc_persistence| .{ .accounts_hash = inc_persistence.incremental_hash, .capitalization = inc_persistence.incremental_capitalization, } else null, @@ -4744,23 +4778,19 @@ pub const BenchmarkAccountsDB = struct { try file.seekTo(0); } - var memory = try std.posix.mmap( - null, - aligned_size, - std.posix.PROT.READ | std.posix.PROT.WRITE, - std.posix.MAP{ .TYPE = .SHARED }, // need it written to the file before it can be used - file.handle, - 0, - ); + const buf = try allocator.alloc(u8, aligned_size); + defer allocator.free(buf); var offset: usize = 0; for (0..n_accounts) |i| { const account = try Account.initRandom(allocator, random, i % 1_000); defer allocator.free(account.data); var pubkey = pubkeys[i % n_accounts]; - offset += account.writeToBuf(&pubkey, memory[offset..]); + offset += account.writeToBuf(&pubkey, buf[offset..]); } + try file.writeAll(buf); + break :blk try AccountFile.init(file, .{ .id = FileId.fromInt(@intCast(s)), .length = offset }, s); }; errdefer account_file.deinit(); diff --git a/src/accountsdb/download.zig b/src/accountsdb/download.zig index d19b5f7d3..48447dc8a 100644 --- a/src/accountsdb/download.zig +++ b/src/accountsdb/download.zig @@ -11,6 +11,13 @@ const ThreadSafeContactInfo = sig.gossip.data.ThreadSafeContactInfo; const GossipService = sig.gossip.GossipService; const Logger = sig.trace.Logger; const ScopedLogger = sig.trace.ScopedLogger; +const LegacyContactInfo = sig.gossip.data.LegacyContactInfo; +const SignedGossipData = sig.gossip.data.SignedGossipData; +const KeyPair = std.crypto.sign.Ed25519.KeyPair; +const SnapshotFiles = sig.accounts_db.SnapshotFiles; +const FullAndIncrementalManifest = sig.accounts_db.FullAndIncrementalManifest; + +const parallelUnpackZstdTarBall = sig.accounts_db.parallelUnpackZstdTarBall; const DOWNLOAD_PROGRESS_UPDATES_NS = 6 * std.time.ns_per_s; @@ -245,7 +252,7 @@ pub fn downloadSnapshotsFromGossip( downloadFile( allocator, - logger.unscoped(), + logger, snapshot_url, output_dir, snapshot_filename, @@ -286,7 +293,7 @@ pub fn downloadSnapshotsFromGossip( logger.info().logf("downloading inc_snapshot from: {s}", .{inc_snapshot_url}); _ = downloadFile( allocator, - logger.unscoped(), + logger, inc_snapshot_url, output_dir, inc_snapshot_filename, @@ -309,7 +316,7 @@ pub fn downloadSnapshotsFromGossip( const DownloadProgress = struct { file: std.fs.File, min_mb_per_second: ?usize, - logger: ScopedLogger(@typeName(Self)), + logger: ScopedLogger(LOG_SCOPE), progress_timer: sig.time.Timer, bytes_read: u64 = 0, @@ -319,7 +326,7 @@ const DownloadProgress = struct { const Self = @This(); fn init( - logger: Logger, + logger: ScopedLogger(LOG_SCOPE), output_dir: std.fs.Dir, filename: []const u8, download_size: usize, @@ -330,7 +337,7 @@ const DownloadProgress = struct { try file.setEndPos(download_size); return .{ - .logger = logger.withScope(@typeName(Self)), + .logger = logger, .file = file, .min_mb_per_second = min_mb_per_second, .progress_timer = try sig.time.Timer.start(), @@ -479,15 +486,14 @@ fn enableProgress( /// downloads a file from a url into output_dir/filename /// returns error if it fails. /// the main errors include {HeaderRequestFailed, NoContentLength, TooSlow} or a curl-related error -pub fn downloadFile( +fn downloadFile( allocator: std.mem.Allocator, - logger_: Logger, + logger: ScopedLogger(LOG_SCOPE), url: [:0]const u8, output_dir: std.fs.Dir, filename: []const u8, min_mb_per_second: ?usize, ) !void { - const logger = logger_.withScope(LOG_SCOPE); var easy = try curl.Easy.init(allocator, .{}); defer easy.deinit(); @@ -509,7 +515,7 @@ pub fn downloadFile( // timeout will need to be larger easy.timeout_ms = std.time.ms_per_hour * 5; // 5 hours is probs too long but its ok var download_progress = try DownloadProgress.init( - logger.unscoped(), + logger, output_dir, filename, download_size, @@ -538,10 +544,181 @@ pub fn downloadFile( } } -const LegacyContactInfo = sig.gossip.data.LegacyContactInfo; -const SignedGossipData = sig.gossip.data.SignedGossipData; +pub fn getOrDownloadAndUnpackSnapshot( + allocator: std.mem.Allocator, + logger_: Logger, + validator_dir: std.fs.Dir, + /// dir which stores the snapshot files to unpack into {validator_dir}/accounts_db + maybe_snapshot_dir: ?std.fs.Dir, + options: struct { + /// gossip service is not needed when loading from an existing snapshot. + /// but when we need to download a new snapshot (force_new_snapshot_download flag), + /// we need the gossip service. + gossip_service: ?*GossipService = null, + force_new_snapshot_download: bool = false, + force_unpack_snapshot: bool = false, + num_threads_snapshot_unpack: u16 = 0, + min_snapshot_download_speed_mbs: usize = 20, + trusted_validators: ?[]const Pubkey = null, + }, +) !struct { FullAndIncrementalManifest, SnapshotFiles } { + const logger = logger_.withScope(LOG_SCOPE); -const KeyPair = std.crypto.sign.Ed25519.KeyPair; + const force_unpack_snapshot = options.force_unpack_snapshot; + const force_new_snapshot_download = options.force_new_snapshot_download; + var n_threads_snapshot_unpack: u32 = options.num_threads_snapshot_unpack; + if (n_threads_snapshot_unpack == 0) { + const n_cpus = @as(u32, @truncate(try std.Thread.getCpuCount())); + n_threads_snapshot_unpack = n_cpus / 2; + } + + // check if we need to download a fresh snapshot + var accounts_db_exists = blk: { + if (validator_dir.openDir(sig.ACCOUNTS_DB_SUBDIR, .{ .iterate = true })) |dir| { + std.posix.close(dir.fd); + break :blk true; + } else |_| { + break :blk false; + } + }; + + // clear old snapshots, if we will download a new one + if (force_new_snapshot_download and accounts_db_exists) { + logger.info().log("deleting accounts_db dir..."); + try validator_dir.deleteTreeMinStackSize("accounts_db"); + accounts_db_exists = false; + } + + const accounts_db_dir = try validator_dir.makeOpenPath(sig.ACCOUNTS_DB_SUBDIR, .{ + .iterate = true, + }); + const snapshot_dir = maybe_snapshot_dir orelse accounts_db_dir; + + // download a new snapshot if required + const snapshot_exists = blk: { + _ = SnapshotFiles.find(allocator, snapshot_dir) catch |err| { + std.debug.print("failed to find snapshot files: {}\n", .{err}); + break :blk false; + }; + break :blk true; + }; + const should_download_snapshot = force_new_snapshot_download or !snapshot_exists; + if (should_download_snapshot) { + const min_mb_per_sec = options.min_snapshot_download_speed_mbs; + const gossip_service = options.gossip_service orelse { + return error.SnapshotsNotFoundAndNoGossipService; + }; + + try downloadSnapshotsFromGossip( + allocator, + logger.unscoped(), + options.trusted_validators, + gossip_service, + snapshot_dir, + @intCast(min_mb_per_sec), + ); + } + + const valid_accounts_folder = blk: { + // NOTE: we only need to check this if we are *not* unpacking a fresh snapshot + if (force_unpack_snapshot or !snapshot_exists) break :blk false; + + // do a quick sanity check on the number of files in accounts/ + // NOTE: this is sometimes the case that you unpacked only a portion + // of the snapshot + var accounts_dir = accounts_db_dir.openDir("accounts", .{}) catch |err| switch (err) { + // accounts folder doesnt exist, so its invalid + error.FileNotFound => break :blk false, + else => return err, + }; + defer accounts_dir.close(); + const n_account_files = (try accounts_dir.stat()).size; + if (n_account_files <= 100) { + // if the accounts/ directory is empty, then we should unpack + // the snapshot to get correct state + logger.info().log("empty accounts/ directory found, will unpack snapshot..."); + break :blk false; + } else { + logger.info().log("accounts/ directory found, will not unpack snapshot..."); + break :blk true; + } + }; + + var timer = try std.time.Timer.start(); + const should_unpack_snapshot = force_unpack_snapshot or !snapshot_exists or !valid_accounts_folder; + if (should_unpack_snapshot) { + const snapshot_files = try SnapshotFiles.find(allocator, snapshot_dir); + if (snapshot_files.incremental_info == null) { + logger.info().log("no incremental snapshot found"); + } + errdefer { + // if something goes wrong while unpacking, delete the accounts/ directory + // so we unpack the full snapshot the next time we run this method. its + // hard to debug with partially unpacked snapshots. + // + // NOTE: if we didnt do this, we would try to startup with a incomplete + // accounts/ directory the next time we ran the code - see `valid_acounts_folder`. + snapshot_dir.deleteTree("accounts") catch |err| { + std.debug.print("failed to delete accounts/ dir: {}\n", .{err}); + }; + } + + logger.info().log("unpacking snapshots..."); + + timer.reset(); + logger.info().logf("unpacking {s}...", .{snapshot_files.full.snapshotArchiveName().constSlice()}); + { + const archive_file = try snapshot_dir.openFile( + snapshot_files.full.snapshotArchiveName().constSlice(), + .{}, + ); + defer archive_file.close(); + try parallelUnpackZstdTarBall( + allocator, + logger.unscoped(), + archive_file, + accounts_db_dir, + n_threads_snapshot_unpack, + true, + ); + } + logger.info().logf("unpacked snapshot in {s}", .{std.fmt.fmtDuration(timer.read())}); + + // TODO: can probs do this in parallel with full snapshot + if (snapshot_files.incremental()) |incremental_snapshot| { + timer.reset(); + logger.info().logf("unpacking {s}...", .{incremental_snapshot.snapshotArchiveName().constSlice()}); + + const archive_file = try snapshot_dir.openFile(incremental_snapshot.snapshotArchiveName().constSlice(), .{}); + defer archive_file.close(); + + try parallelUnpackZstdTarBall( + allocator, + logger.unscoped(), + archive_file, + accounts_db_dir, + n_threads_snapshot_unpack, + false, + ); + logger.info().logf("unpacked snapshot in {s}", .{std.fmt.fmtDuration(timer.read())}); + } + } else { + logger.info().log("not unpacking snapshot..."); + } + + timer.reset(); + logger.info().log("reading snapshot metadata..."); + const snapshot_files = try SnapshotFiles.find(allocator, snapshot_dir); + const snapshot_fields = try FullAndIncrementalManifest.fromFiles( + allocator, + logger.unscoped(), + accounts_db_dir, + snapshot_files, + ); + logger.info().logf("read snapshot metdata in {s}", .{std.fmt.fmtDuration(timer.read())}); + + return .{ snapshot_fields, snapshot_files }; +} test "accounts_db.download: test remove untrusted peers" { const allocator = std.testing.allocator; diff --git a/src/accountsdb/fuzz.zig b/src/accountsdb/fuzz.zig index c69fb7f37..44a48d139 100644 --- a/src/accountsdb/fuzz.zig +++ b/src/accountsdb/fuzz.zig @@ -81,7 +81,10 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { // NOTE: sometimes this can take a long time so we print when we start and finish std.debug.print("deleting snapshot dir...\n", .{}); test_data_dir.deleteTreeMinStackSize(snapshot_dir_name) catch |err| { - std.debug.print("failed to delete snapshot dir ('{s}'): {}\n", .{ sig.utils.fmt.tryRealPath(snapshot_dir, "."), err }); + std.debug.print( + "failed to delete snapshot dir ('{s}'): {}\n", + .{ sig.utils.fmt.tryRealPath(snapshot_dir, "."), err }, + ); }; std.debug.print("deleted snapshot dir\n", .{}); } @@ -172,8 +175,10 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { var tracked_account = try TrackedAccount.initRandom(random, slot); const existing_pubkey = random.boolean(); - if ((existing_pubkey and tracked_accounts.count() > 0) or update_all_existing) { - const index = random.intRangeAtMost(usize, 0, tracked_accounts.count() - 1); + if ((existing_pubkey and tracked_accounts.count() > 0) or + update_all_existing) + { + const index = random.intRangeLessThan(usize, 0, tracked_accounts.count()); const key = tracked_accounts.keys()[index]; // only if the pubkey is not already in this slot if (!pubkeys_this_slot.contains(key)) { @@ -209,11 +214,16 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const key = tracked_accounts.keys()[index]; const tracked_account = tracked_accounts.get(key).?; - var account, const ref = try accounts_db.getAccountAndReference(&tracked_account.pubkey); + const account, const ref = + try accounts_db.getAccountAndReference(&tracked_account.pubkey); defer account.deinit(allocator); if (!std.mem.eql(u8, &tracked_account.data, account.data)) { - std.debug.panic("found account {any} with different data: tracked: {any} vs found: {any} ({any})\n", .{ key, tracked_account.data, account.data, ref }); + std.debug.panic( + "found account {} with different data: " ++ + "tracked: {any} vs found: {any} ({})\n", + .{ key, tracked_account.data, account.data, ref }, + ); } }, } @@ -227,17 +237,22 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { snapshot_validation: { // holding the lock here means that the snapshot archive(s) wont be deleted // since deletion requires a write lock - const maybe_latest_snapshot_info, var snapshot_info_lg = accounts_db.latest_snapshot_gen_info.readWithLock(); + const maybe_latest_snapshot_info, // + var snapshot_info_lg // + = accounts_db.latest_snapshot_gen_info.readWithLock(); defer snapshot_info_lg.unlock(); - const snapshot_info = maybe_latest_snapshot_info.* orelse break :snapshot_validation; // no snapshot yet + const snapshot_info = maybe_latest_snapshot_info.* orelse + break :snapshot_validation; // no snapshot yet const full_snapshot_info = snapshot_info.full; // copy the archive to the alternative snapshot dir const full_snapshot_file_info: FullSnapshotFileInfo = full: { if (full_snapshot_info.slot <= last_full_snapshot_validated_slot) { const inc_snapshot_info = snapshot_info.inc orelse break :snapshot_validation; - if (inc_snapshot_info.slot <= last_inc_snapshot_validated_slot) break :snapshot_validation; + if (inc_snapshot_info.slot <= last_inc_snapshot_validated_slot) { + break :snapshot_validation; + } } else { last_full_snapshot_validated_slot = full_snapshot_info.slot; } @@ -249,7 +264,8 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const full_archive_name_bounded = full_snapshot_file_info.snapshotArchiveName(); const full_archive_name = full_archive_name_bounded.constSlice(); - const full_archive_file = try snapshot_dir.openFile(full_archive_name, .{ .mode = .read_only }); + const full_archive_file = + try snapshot_dir.openFile(full_archive_name, .{ .mode = .read_only }); defer full_archive_file.close(); try sig.accounts_db.snapshots.parallelUnpackZstdTarBall( @@ -260,7 +276,10 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { 5, true, ); - logger.info().logf("fuzz[validate]: unpacked full snapshot at slot: {}", .{full_snapshot_info.slot}); + logger.info().logf( + "fuzz[validate]: unpacked full snapshot at slot: {}", + .{full_snapshot_info.slot}, + ); break :full full_snapshot_file_info; }; @@ -281,8 +300,15 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const inc_archive_name_bounded = inc_snapshot_file_info.snapshotArchiveName(); const inc_archive_name = inc_archive_name_bounded.constSlice(); - try snapshot_dir.copyFile(inc_archive_name, alternative_snapshot_dir, inc_archive_name, .{}); - const inc_archive_file = try alternative_snapshot_dir.openFile(inc_archive_name, .{}); + try snapshot_dir.copyFile( + inc_archive_name, + alternative_snapshot_dir, + inc_archive_name, + .{}, + ); + + const inc_archive_file = + try alternative_snapshot_dir.openFile(inc_archive_name, .{}); defer inc_archive_file.close(); try sig.accounts_db.snapshots.parallelUnpackZstdTarBall( @@ -293,7 +319,10 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { 5, true, ); - logger.info().logf("fuzz[validate]: unpacked inc snapshot at slot: {}", .{inc_snapshot_info.slot}); + logger.info().logf( + "fuzz[validate]: unpacked inc snapshot at slot: {}", + .{inc_snapshot_info.slot}, + ); break :inc inc_snapshot_file_info; }; @@ -303,13 +332,13 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { maybe_incremental_file_info, ); - var snapshot_fields = try sig.accounts_db.AllSnapshotFields.fromFiles( + const combined_manifest = try sig.accounts_db.FullAndIncrementalManifest.fromFiles( allocator, logger, alternative_snapshot_dir, snapshot_files, ); - defer snapshot_fields.deinit(allocator); + defer combined_manifest.deinit(allocator); var alt_accounts_db = try AccountsDB.init(.{ .allocator = allocator, @@ -323,17 +352,21 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { }); defer alt_accounts_db.deinit(); - _ = try alt_accounts_db.loadWithDefaults( + (try alt_accounts_db.loadWithDefaults( allocator, - &snapshot_fields, + combined_manifest, 1, true, N_ACCOUNTS_PER_SLOT, false, false, - ); + )).deinit(allocator); + const maybe_inc_slot = if (snapshot_info.inc) |inc| inc.slot else null; - logger.info().logf("loaded and validated snapshot at slot: {} (and inc snapshot @ slot {any})", .{ full_snapshot_info.slot, maybe_inc_slot }); + logger.info().logf( + "loaded and validated snapshot at slot: {} (and inc snapshot @ slot {any})", + .{ full_snapshot_info.slot, maybe_inc_slot }, + ); } } diff --git a/src/accountsdb/fuzz_snapshot.zig b/src/accountsdb/fuzz_snapshot.zig index 8e98fa56b..9a4183dba 100644 --- a/src/accountsdb/fuzz_snapshot.zig +++ b/src/accountsdb/fuzz_snapshot.zig @@ -5,7 +5,7 @@ const bincode = sig.bincode; const Slot = sig.core.Slot; const Hash = sig.core.Hash; -const SnapshotFields = sig.accounts_db.SnapshotFields; +const SnapshotManifest = sig.accounts_db.Manifest; const FileId = sig.accounts_db.accounts_file.FileId; const AccountsDbFields = sig.accounts_db.snapshots.AccountsDbFields; const BankFields = sig.accounts_db.snapshots.BankFields; @@ -44,16 +44,16 @@ pub fn run(args: *std.process.ArgIterator) !void { while (timer.read() < MAX_FUZZ_TIME_NS) : (i += 1) { bytes_buffer.clearRetainingCapacity(); - const snapshot_original: SnapshotFields = try randomSnapshotFields(allocator, random); - defer snapshot_original.deinit(allocator); + const manifest_original: SnapshotManifest = try randomSnapshotManifest(allocator, random); + defer manifest_original.deinit(allocator); - try bytes_buffer.ensureUnusedCapacity(bincode.sizeOf(snapshot_original, .{}) * 2); + try bytes_buffer.ensureUnusedCapacity(bincode.sizeOf(manifest_original, .{}) * 2); const original_bytes_start = bytes_buffer.items.len; - try bincode.write(bytes_buffer.writer(), snapshot_original, .{}); + try bincode.write(bytes_buffer.writer(), manifest_original, .{}); const original_bytes_end = bytes_buffer.items.len; - const snapshot_deserialized = try bincode.readFromSlice(allocator, SnapshotFields, bytes_buffer.items[original_bytes_start..original_bytes_end], .{}); + const snapshot_deserialized = try bincode.readFromSlice(allocator, SnapshotManifest, bytes_buffer.items[original_bytes_start..original_bytes_end], .{}); defer snapshot_deserialized.deinit(allocator); const serialized_bytes_start = bytes_buffer.items.len; @@ -69,12 +69,12 @@ pub fn run(args: *std.process.ArgIterator) !void { const max_list_entries = 1 << 8; -fn randomSnapshotFields( +fn randomSnapshotManifest( allocator: std.mem.Allocator, /// Should be a PRNG, not a true RNG. See the documentation on `std.Random.uintLessThan` /// for commentary on the runtime of this function. random: std.Random, -) !SnapshotFields { +) !SnapshotManifest { const bank_fields = try BankFields.initRandom(allocator, random, max_list_entries); errdefer bank_fields.deinit(allocator); diff --git a/src/accountsdb/index.zig b/src/accountsdb/index.zig index 1b29c8792..d4d68d3cd 100644 --- a/src/accountsdb/index.zig +++ b/src/accountsdb/index.zig @@ -11,6 +11,9 @@ const DiskMemoryAllocator = sig.utils.allocators.DiskMemoryAllocator; const createAndMmapFile = sig.utils.allocators.createAndMmapFile; +const LOG_SCOPE = "accounts_db.index"; +const ScopedLogger = sig.trace.ScopedLogger(LOG_SCOPE); + /// reference to an account (either in a file or in the unrooted_map) pub const AccountRef = struct { pubkey: Pubkey, @@ -51,6 +54,7 @@ pub const AccountRef = struct { /// Analogous to [AccountsIndex](https://github.com/anza-xyz/agave/blob/a6b2283142192c5360ad0f53bec1eb4a9fb36154/accounts-db/src/accounts_index.rs#L644) pub const AccountIndex = struct { allocator: std.mem.Allocator, + logger: ScopedLogger, /// map from Pubkey -> AccountRefHead pubkey_ref_map: ShardedPubkeyRefMap, @@ -83,7 +87,7 @@ pub const AccountIndex = struct { /// number of shards for the pubkey_ref_map number_of_shards: usize, ) !Self { - const logger = logger_.withScope(@typeName((Self))); + const logger = logger_.withScope(LOG_SCOPE); const reference_allocator: ReferenceAllocator = switch (allocator_config) { .ram => |ram| blk: { logger.info().logf("using ram memory for account index", .{}); @@ -110,6 +114,7 @@ pub const AccountIndex = struct { return .{ .allocator = allocator, + .logger = logger, .pubkey_ref_map = try ShardedPubkeyRefMap.init(allocator, number_of_shards), .slot_reference_map = RwMux(SlotRefMap).init(SlotRefMap.init(allocator)), .reference_allocator = reference_allocator, @@ -302,12 +307,11 @@ pub const AccountIndex = struct { } } - pub fn loadFromDisk(self: *Self, dir: std.fs.Dir, logger: sig.trace.Logger) !void { - const scoped_logger = logger.withScope("load index state"); - + pub fn loadFromDisk(self: *Self, dir: std.fs.Dir) !void { // manager must be empty std.debug.assert(self.reference_manager.capacity == 0); + self.logger.info().log("loading state from disk..."); const reference_file = try dir.openFile("index.bin", .{}); const size = (try reference_file.stat()).size; const index_memory = try std.posix.mmap( @@ -339,7 +343,7 @@ pub const AccountIndex = struct { offset += records_size; // load the []AccountRef - scoped_logger.info().log("loading account references"); + self.logger.info().log("loading account references"); const references = try sig.bincode.readFromSlice( // NOTE: this still ensure reference memory is either on disk or ram // even though the state we are loading from is on disk. @@ -353,7 +357,7 @@ pub const AccountIndex = struct { self.reference_manager.capacity += references.len; // update the pointers of the references - scoped_logger.info().log("organizing manager memory"); + self.logger.info().log("organizing manager memory"); for (references) |*ref| { if (ref.next_index != null) { ref.next_ptr = &references[ref.next_index.?]; @@ -361,7 +365,7 @@ pub const AccountIndex = struct { } // load the records - scoped_logger.info().log("loading manager records"); + self.logger.info().log("loading manager records"); const records = try sig.bincode.readFromSlice( self.reference_manager.records.allocator, @TypeOf(self.reference_manager.records), @@ -377,7 +381,7 @@ pub const AccountIndex = struct { self.reference_manager.records = records; // load the pubkey_ref_map - scoped_logger.info().log("loading pubkey -> ref map"); + self.logger.info().log("loading pubkey -> ref map"); offset = 0; for (self.pubkey_ref_map.shards) |*shard_rw| { const shard, var lock = shard_rw.writeWithLock(); @@ -406,10 +410,9 @@ pub const AccountIndex = struct { } } - pub fn saveToDisk(self: *Self, dir: std.fs.Dir, logger: sig.trace.Logger) !void { - const scoped_logger = logger.withScope("save index state"); - - scoped_logger.info().log("saving pubkey -> reference map"); + pub fn saveToDisk(self: *Self, dir: std.fs.Dir) !void { + self.logger.info().log("saving state to disk..."); + self.logger.info().log("saving pubkey -> reference map"); // write the pubkey_ref_map (populating this is very expensive) var shard_data_total: u64 = 0; for (self.pubkey_ref_map.shards) |*shard_rw| { @@ -448,7 +451,7 @@ pub const AccountIndex = struct { offset += records_size; // write each shard's data - scoped_logger.info().log("saving pubkey_ref_map memory"); + self.logger.info().log("saving pubkey_ref_map memory"); offset = 0; for (self.pubkey_ref_map.shards) |*shard_rw| { const shard, var lock = shard_rw.readWithLock(); @@ -462,7 +465,7 @@ pub const AccountIndex = struct { offset += shard_memory.len; } - scoped_logger.info().log("saving account references"); + self.logger.info().log("saving account references"); offset = 0; // collapse [][]AccountRef into a single slice std.mem.writeInt( @@ -479,7 +482,7 @@ pub const AccountIndex = struct { } } - scoped_logger.info().log("saving reference_manager records"); + self.logger.info().log("saving reference_manager records"); _ = try sig.bincode.writeToSlice(records_memory, self.reference_manager.records.items, .{}); } }; @@ -779,7 +782,7 @@ test "save and load account index state -- multi linked list" { } // save the state - try index.saveToDisk(save_dir.dir, .noop); + try index.saveToDisk(save_dir.dir); var index2 = try AccountIndex.init( allocator, @@ -792,7 +795,7 @@ test "save and load account index state -- multi linked list" { // load the state // NOTE: this will work even if something is wrong // because were using the same pointers because its the same run - try index2.loadFromDisk(save_dir.dir, .noop); + try index2.loadFromDisk(save_dir.dir); { const ref_head, var ref_head_lg = index2.pubkey_ref_map.getRead(&ref_a.pubkey).?; defer ref_head_lg.unlock(); @@ -832,7 +835,7 @@ test "save and load account index state" { } // save the state - try index.saveToDisk(save_dir.dir, .noop); + try index.saveToDisk(save_dir.dir); var index2 = try AccountIndex.init( allocator, @@ -845,7 +848,7 @@ test "save and load account index state" { // load the state // NOTE: this will work even if something is wrong // because were using the same pointers because its the same run - try index2.loadFromDisk(save_dir.dir, .noop); + try index2.loadFromDisk(save_dir.dir); { const ref_head, var ref_head_lg = index2.pubkey_ref_map.getRead(&ref_a.pubkey).?; defer ref_head_lg.unlock(); diff --git a/src/accountsdb/lib.zig b/src/accountsdb/lib.zig index 487e7750d..4e989d0a3 100644 --- a/src/accountsdb/lib.zig +++ b/src/accountsdb/lib.zig @@ -1,21 +1,22 @@ pub const accounts_file = @import("accounts_file.zig"); pub const bank = @import("bank.zig"); +pub const buffer_pool = @import("buffer_pool.zig"); pub const cache = @import("cache.zig"); pub const db = @import("db.zig"); pub const download = @import("download.zig"); +pub const fuzz = @import("fuzz.zig"); +pub const fuzz_snapshot = @import("fuzz_snapshot.zig"); pub const genesis_config = @import("genesis_config.zig"); pub const index = @import("index.zig"); pub const snapshots = @import("snapshots.zig"); -pub const sysvars = @import("sysvars.zig"); -pub const fuzz = @import("fuzz.zig"); -pub const fuzz_snapshot = @import("fuzz_snapshot.zig"); pub const swiss_map = @import("swiss_map.zig"); +pub const sysvars = @import("sysvars.zig"); pub const AccountsDB = db.AccountsDB; -pub const AllSnapshotFields = snapshots.AllSnapshotFields; +pub const FullAndIncrementalManifest = snapshots.FullAndIncrementalManifest; pub const Bank = bank.Bank; pub const GenesisConfig = genesis_config.GenesisConfig; -pub const SnapshotFields = snapshots.SnapshotFields; +pub const Manifest = snapshots.Manifest; pub const SnapshotFiles = snapshots.SnapshotFiles; pub const StatusCache = snapshots.StatusCache; pub const ClusterType = genesis_config.ClusterType; diff --git a/src/accountsdb/readme.md b/src/accountsdb/readme.md index 0905b3f27..3c6471198 100644 --- a/src/accountsdb/readme.md +++ b/src/accountsdb/readme.md @@ -331,7 +331,7 @@ The core logic for generating a snapshot lives in `accounts_db.db.writeSnapshotT The procedure consists of writing the version file, the status cache (`snapshots/status_cache`) file, the snapshot manifest (`snapshots/{SLOT}/{SLOT}`), and the account files (`accounts/{SLOT}.{FILE_ID}`). This is all written to a stream in the TAR archive format. -The snapshot manifest file content is comprised of the bincoded (bincode-encoded) data structure `SnapshotFields`, which is an aggregate of: +The snapshot manifest file content is comprised of the bincoded (bincode-encoded) data structure `Manifest`, which is an aggregate of: * implicit state: data derived from the current state of AccountsDB, like the file map for all the account which exist at that snapshot, or which have changed relative to a full snapshot in an incremental one * configuration state: data that is used to communicate details about the snapshot, like the full slot to which an incremental snapshot is relative. diff --git a/src/accountsdb/snapshots.zig b/src/accountsdb/snapshots.zig index b710bdaef..d0c0844e8 100644 --- a/src/accountsdb/snapshots.zig +++ b/src/accountsdb/snapshots.zig @@ -24,8 +24,6 @@ const SlotHistory = sig.accounts_db.sysvars.SlotHistory; const Logger = sig.trace.Logger; -const parallelUntarToFileSystem = sig.utils.tar.parallelUntarToFileSystem; - pub const MAXIMUM_ACCOUNT_FILE_SIZE: u64 = 16 * 1024 * 1024 * 1024; // 16 GiB pub const MAX_RECENT_BLOCKHASHES: usize = 300; pub const MAX_CACHE_ENTRIES: usize = MAX_RECENT_BLOCKHASHES; @@ -49,63 +47,148 @@ pub const StakeHistoryEntry = struct { } }; -pub const EpochAndStakeHistoryEntry = struct { Epoch, StakeHistoryEntry }; +pub const EpochAndStakeHistoryEntry = struct { + epoch: Epoch, + history_entry: StakeHistoryEntry, -pub fn epochAndStakeHistoryEntryRandom(random: std.Random) EpochAndStakeHistoryEntry { - return .{ random.int(Epoch), StakeHistoryEntry.initRandom(random) }; -} + pub fn initRandom(random: std.Random) EpochAndStakeHistoryEntry { + return .{ + .epoch = random.int(Epoch), + .history_entry = StakeHistoryEntry.initRandom(random), + }; + } +}; /// Analogous to [StakeHistory](https://github.com/anza-xyz/agave/blob/5a9906ebf4f24cd2a2b15aca638d609ceed87797/sdk/program/src/stake_history.rs#L62) -pub const StakeHistory = []const EpochAndStakeHistoryEntry; +pub const EpochAndStakeHistory = []const EpochAndStakeHistoryEntry; pub fn stakeHistoryRandom( random: std.Random, allocator: std.mem.Allocator, max_list_entries: usize, -) std.mem.Allocator.Error!StakeHistory { - const StakeHistoryItem = struct { Epoch, StakeHistoryEntry }; +) std.mem.Allocator.Error!EpochAndStakeHistory { const stake_history_len = random.uintAtMost(usize, max_list_entries); - const stake_history = try allocator.alloc(StakeHistoryItem, stake_history_len); + const stake_history = try allocator.alloc(EpochAndStakeHistoryEntry, stake_history_len); errdefer allocator.free(stake_history); - for (stake_history) |*entry| entry.* = epochAndStakeHistoryEntryRandom(random); + for (stake_history) |*entry| entry.* = EpochAndStakeHistoryEntry.initRandom(random); return stake_history; } +pub const StakeAndVoteAccount = struct { u64, VoteAccount }; + +pub const StakeAndVoteAccountsMap = std.AutoArrayHashMapUnmanaged(Pubkey, StakeAndVoteAccount); + +pub fn stakeAndVoteAccountsMapDeinit( + map: StakeAndVoteAccountsMap, + allocator: std.mem.Allocator, +) void { + var copy = map; + for (copy.values()) |stake_and_vote_account| { + _, const vote_account = stake_and_vote_account; + vote_account.deinit(allocator); + } + copy.deinit(allocator); +} + +pub fn stakeAndVoteAccountsMapClone( + map: StakeAndVoteAccountsMap, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!StakeAndVoteAccountsMap { + var cloned: StakeAndVoteAccountsMap = .{}; + errdefer stakeAndVoteAccountsMapDeinit(cloned, allocator); + + try cloned.ensureTotalCapacity(allocator, map.count()); + for (map.keys(), map.values()) |key, value| { + const stake, const vote_account = value; + const vote_account_cloned = try vote_account.clone(allocator); + cloned.putAssumeCapacityNoClobber(key, .{ stake, vote_account_cloned }); + } + + return cloned; +} + +pub fn stakeAndVoteAccountsMapRandom( + random: std.Random, + allocator: std.mem.Allocator, + max_list_entries: usize, +) std.mem.Allocator.Error!StakeAndVoteAccountsMap { + var result: StakeAndVoteAccountsMap = .{}; + errdefer stakeAndVoteAccountsMapDeinit(result, allocator); + + const entry_count = random.uintAtMost(usize, max_list_entries); + try result.ensureTotalCapacity(allocator, entry_count); + for (0..entry_count) |_| { + const key = Pubkey.initRandom(random); + const gop = result.getOrPutAssumeCapacity(key); + if (gop.found_existing) continue; + const value = try VoteAccount.initRandom( + random, + allocator, + max_list_entries, + error{ RandomError1, RandomError2, RandomError3 }, + ); + gop.value_ptr.* = .{ random.int(u64), value }; + } + + return result; +} + /// Analogous to [VoteAccounts](https://github.com/anza-xyz/agave/blob/cadba689cb44db93e9c625770cafd2fc0ae89e33/vote/src/vote_account.rs#L44) pub const VoteAccounts = struct { - vote_accounts: std.AutoArrayHashMapUnmanaged(Pubkey, StakeAndVoteAccount), - staked_nodes: ?std.AutoArrayHashMapUnmanaged( - Pubkey, // VoteAccount.vote_state.node_pubkey. - u64, // Total stake across all vote-accounts. - ) = null, + accounts: StakeAndVoteAccountsMap, + staked_nodes: ?StakedNodesMap, - pub const @"!bincode-config:staked_nodes" = bincode.FieldConfig(?std.AutoArrayHashMapUnmanaged(Pubkey, u64)){ .skip = true }; - - const Self = @This(); + pub const @"!bincode-config:staked_nodes" = bincode.FieldConfig(?StakedNodesMap){ + .skip = true, + .default_value = @as(?StakedNodesMap, null), + }; - pub const StakeAndVoteAccount = struct { u64, VoteAccount }; + pub const StakedNodesMap = std.AutoArrayHashMapUnmanaged( + Pubkey, // VoteAccount.vote_state.node_pubkey. + u64, // Total stake across all vote-accounts. + ); - pub fn deinit(vote_accounts: VoteAccounts, allocator: std.mem.Allocator) void { + pub fn deinit( + vote_accounts: VoteAccounts, + allocator: std.mem.Allocator, + ) void { var copy = vote_accounts; - for (copy.vote_accounts.values()) |entry| { + for (copy.accounts.values()) |entry| { _, const vote_account = entry; vote_account.deinit(allocator); } - copy.vote_accounts.deinit(allocator); + copy.accounts.deinit(allocator); if (copy.staked_nodes) |*staked_nodes| { staked_nodes.deinit(allocator); } } - pub fn stakedNodes(self: *Self, allocator: std.mem.Allocator) !*const std.AutoArrayHashMapUnmanaged(Pubkey, u64) { + pub fn clone( + vote_accounts: VoteAccounts, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!VoteAccounts { + const accounts = try stakeAndVoteAccountsMapClone(vote_accounts.accounts, allocator); + errdefer stakeAndVoteAccountsMapDeinit(accounts, allocator); + + var staked_nodes: ?StakedNodesMap = + if (vote_accounts.staked_nodes) |map| try map.clone(allocator) else null; + errdefer if (staked_nodes) |*map| map.deinit(allocator); + + return .{ + .accounts = accounts, + .staked_nodes = staked_nodes, + }; + } + + pub fn stakedNodes(self: *VoteAccounts, allocator: std.mem.Allocator) !*const StakedNodesMap { if (self.staked_nodes) |*staked_nodes| { return staked_nodes; } - const vote_accounts = self.vote_accounts; + const vote_accounts = self.accounts; var staked_nodes = std.AutoArrayHashMap(Pubkey, u64).init(allocator); var iter = vote_accounts.iterator(); while (iter.next()) |vote_entry| { @@ -126,7 +209,7 @@ pub const VoteAccounts = struct { allocator: std.mem.Allocator, max_list_entries: usize, ) std.mem.Allocator.Error!VoteAccounts { - var stakes_vote_accounts = std.AutoArrayHashMap(Pubkey, VoteAccounts.StakeAndVoteAccount).init(allocator); + var stakes_vote_accounts = StakeAndVoteAccountsMap.Managed.init(allocator); errdefer stakes_vote_accounts.deinit(); errdefer for (stakes_vote_accounts.values()) |pair| { @@ -142,7 +225,12 @@ pub const VoteAccounts = struct { return Pubkey.initRandom(rand); } pub fn randomValue(ctx: @This(), rand: std.Random) !StakeAndVoteAccount { - const vote_account: VoteAccount = try VoteAccount.initRandom(rand, ctx.allocator, ctx.max_list_entries, error{ RandomError1, RandomError2, RandomError3 }); + const vote_account: VoteAccount = try VoteAccount.initRandom( + rand, + ctx.allocator, + ctx.max_list_entries, + error{ RandomError1, RandomError2, RandomError3 }, + ); errdefer vote_account.deinit(ctx.allocator); return .{ rand.int(u64), vote_account }; } @@ -154,17 +242,24 @@ pub const VoteAccounts = struct { var stakes_maybe_staked_nodes = if (random.boolean()) std.AutoArrayHashMap(Pubkey, u64).init(allocator) else null; errdefer if (stakes_maybe_staked_nodes) |*staked_nodes| staked_nodes.deinit(); - if (stakes_maybe_staked_nodes) |*staked_nodes| try sig.rand.fillHashmapWithRng(staked_nodes, random, random.uintAtMost(usize, max_list_entries), struct { - pub fn randomKey(rand: std.Random) !Pubkey { - return Pubkey.initRandom(rand); - } - pub fn randomValue(rand: std.Random) !u64 { - return rand.int(u64); - } - }); + if (stakes_maybe_staked_nodes) |*staked_nodes| { + try sig.rand.fillHashmapWithRng( + staked_nodes, + random, + random.uintAtMost(usize, max_list_entries), + struct { + pub fn randomKey(rand: std.Random) !Pubkey { + return Pubkey.initRandom(rand); + } + pub fn randomValue(rand: std.Random) !u64 { + return rand.int(u64); + } + }, + ); + } return .{ - .vote_accounts = stakes_vote_accounts.unmanaged, + .accounts = stakes_vote_accounts.unmanaged, .staked_nodes = if (stakes_maybe_staked_nodes) |staked_nodes| staked_nodes.unmanaged else null, }; } @@ -180,12 +275,35 @@ pub const VoteAccount = struct { vote_account.account.deinit(allocator); } + pub fn clone( + vote_account: VoteAccount, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!VoteAccount { + const account = try vote_account.account.clone(allocator); + errdefer account.deinit(allocator); + return .{ + .account = account, + .vote_state = vote_account.vote_state, + }; + } + pub fn voteState(self: *@This()) !VoteState { if (self.vote_state) |vs| { return vs; } - self.vote_state = bincode.readFromSlice(undefined, VoteState, self.account.data, .{}); - return self.vote_state.?; + const assert_alloc = sig.utils.allocators.failing.allocator(.{ + .alloc = .assert, + .resize = .assert, + .free = .assert, + }); + const vote_state = bincode.readFromSlice( + assert_alloc, + VoteState, + self.account.data, + .{}, + ); + self.vote_state = vote_state; + return vote_state; } pub fn initRandom( @@ -341,6 +459,20 @@ pub const BlockhashQueue = struct { ages.deinit(allocator); } + pub fn clone( + bhq: BlockhashQueue, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!BlockhashQueue { + var ages = try bhq.ages.clone(allocator); + errdefer ages.deinit(allocator); + return .{ + .last_hash_index = bhq.last_hash_index, + .last_hash = bhq.last_hash, + .ages = ages, + .max_age = bhq.max_age, + }; + } + pub fn initRandom( random: std.Random, allocator: std.mem.Allocator, @@ -364,6 +496,12 @@ pub const UnusedAccounts = struct { unused2: std.AutoArrayHashMapUnmanaged(Pubkey, void), unused3: std.AutoArrayHashMapUnmanaged(Pubkey, u64), + pub const EMPTY: UnusedAccounts = .{ + .unused1 = .{}, + .unused2 = .{}, + .unused3 = .{}, + }; + pub fn deinit(unused_accounts: UnusedAccounts, allocator: std.mem.Allocator) void { var copy = unused_accounts; copy.unused1.deinit(allocator); @@ -371,6 +509,26 @@ pub const UnusedAccounts = struct { copy.unused3.deinit(allocator); } + pub fn clone( + unused_accounts: UnusedAccounts, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!UnusedAccounts { + var unused1 = try unused_accounts.unused1.clone(allocator); + errdefer unused1.deinit(allocator); + + var unused2 = try unused_accounts.unused2.clone(allocator); + errdefer unused2.deinit(allocator); + + var unused3 = try unused_accounts.unused3.clone(allocator); + errdefer unused3.deinit(allocator); + + return .{ + .unused1 = unused1, + .unused2 = unused2, + .unused3 = unused3, + }; + } + pub fn initRandom( random: std.Random, allocator: std.mem.Allocator, @@ -441,6 +599,13 @@ pub const HardForks = struct { allocator.free(hard_forks.items); } + pub fn clone( + hard_forks: HardForks, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!HardForks { + return .{ .items = try allocator.dupe(SlotAndCount, hard_forks.items) }; + } + pub fn initRandom( random: std.Random, allocator: std.mem.Allocator, @@ -469,6 +634,16 @@ pub const NodeVoteAccounts = struct { allocator.free(node_vote_accounts.vote_accounts); } + pub fn clone( + node_vote_accounts: NodeVoteAccounts, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!NodeVoteAccounts { + return .{ + .vote_accounts = try allocator.dupe(Pubkey, node_vote_accounts.vote_accounts), + .total_stake = node_vote_accounts.total_stake, + }; + } + pub fn initRandom( random: std.Random, allocator: std.mem.Allocator, @@ -487,7 +662,10 @@ pub const NodeVoteAccounts = struct { /// Analogous to [NodeIdToVoteAccounts](https://github.com/anza-xyz/agave/blob/8d1ef48c785a5d9ee5c0df71dc520ee1a49d8168/runtime/src/epoch_stakes.rs#L9) pub const NodeIdToVoteAccountsMap = std.AutoArrayHashMapUnmanaged(Pubkey, NodeVoteAccounts); -pub fn nodeIdToVoteAccountsMapDeinit(map: NodeIdToVoteAccountsMap, allocator: std.mem.Allocator) void { +pub fn nodeIdToVoteAccountsMapDeinit( + map: NodeIdToVoteAccountsMap, + allocator: std.mem.Allocator, +) void { for (map.values()) |*node_vote_accounts| { node_vote_accounts.deinit(allocator); } @@ -495,6 +673,21 @@ pub fn nodeIdToVoteAccountsMapDeinit(map: NodeIdToVoteAccountsMap, allocator: st copy.deinit(allocator); } +pub fn nodeIdToVoteAccountsMapClone( + map: NodeIdToVoteAccountsMap, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!NodeIdToVoteAccountsMap { + var cloned: NodeIdToVoteAccountsMap = .{}; + errdefer nodeIdToVoteAccountsMapDeinit(cloned, allocator); + + try cloned.ensureTotalCapacity(allocator, map.count()); + for (map.keys(), map.values()) |key, value| { + cloned.putAssumeCapacityNoClobber(key, try value.clone(allocator)); + } + + return cloned; +} + pub fn nodeIdToVoteAccountsMapRandom( allocator: std.mem.Allocator, random: std.Random, @@ -547,7 +740,7 @@ pub fn epochAuthorizedVotersRandom( /// Analogous to [EpochStakes](https://github.com/anza-xyz/agave/blob/574bae8fefc0ed256b55340b9d87b7689bcdf222/runtime/src/epoch_stakes.rs#L22) pub const EpochStakes = struct { - stakes: Stakes(Delegation), + stakes: Stakes(.delegation), total_stake: u64, node_id_to_vote_accounts: NodeIdToVoteAccountsMap, epoch_authorized_voters: EpochAuthorizedVoters, @@ -560,6 +753,30 @@ pub const EpochStakes = struct { epoch_authorized_voters.deinit(allocator); } + pub fn clone( + epoch_stakes: EpochStakes, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!EpochStakes { + const stakes = try epoch_stakes.stakes.clone(allocator); + errdefer stakes.deinit(allocator); + + const node_id_to_vote_accounts = try nodeIdToVoteAccountsMapClone( + epoch_stakes.node_id_to_vote_accounts, + allocator, + ); + errdefer nodeIdToVoteAccountsMapDeinit(node_id_to_vote_accounts, allocator); + + var epoch_authorized_voters = try epoch_stakes.epoch_authorized_voters.clone(allocator); + errdefer epoch_authorized_voters.deinit(allocator); + + return .{ + .stakes = stakes, + .total_stake = epoch_stakes.total_stake, + .node_id_to_vote_accounts = node_id_to_vote_accounts, + .epoch_authorized_voters = epoch_authorized_voters, + }; + } + pub fn initRandom( allocator: std.mem.Allocator, /// Should be a PRNG, not a true RNG. See the documentation on `std.Random.uintLessThan` @@ -567,16 +784,7 @@ pub const EpochStakes = struct { random: std.Random, max_list_entries: usize, ) std.mem.Allocator.Error!EpochStakes { - var result_stakes = try Stakes(Delegation).initRandom( - allocator, - random, - max_list_entries, - struct { - pub fn randomValue(rand: std.Random) !Delegation { - return Delegation.initRandom(rand); - } - }, - ); + var result_stakes = try Stakes(.delegation).initRandom(allocator, random, max_list_entries); errdefer result_stakes.deinit(allocator); const node_id_to_vote_accounts = try nodeIdToVoteAccountsMapRandom(allocator, random, max_list_entries); @@ -698,74 +906,81 @@ pub const Stake = struct { } }; -/// Analogous to [StakeStateV2](https://github.com/anza-xyz/agave/blob/8d1ef48c785a5d9ee5c0df71dc520ee1a49d8168/sdk/program/src/stake/state.rs#L145) -pub const StakeStateV2 = union(enum) { - uninitialized, - initialized: Meta, - stake: struct { Meta, Stake, StakeFlags }, - rewards_pool, - - pub const Meta = struct { - rent_exempt_reserve: u64, - authorized: Authorized, - lockup: Lockup, +pub const StakesDelegationElement = enum { + delegation, + stake, - pub fn initRandom(random: std.Random) Meta { - return .{ - .rent_exempt_reserve = random.int(u64), - .authorized = Authorized.initRandom(random), - .lockup = Lockup.initRandom(random), - }; - } - }; - - /// Analogous to [StakeFlags](https://github.com/anza-xyz/agave/blob/8d1ef48c785a5d9ee5c0df71dc520ee1a49d8168/sdk/program/src/stake/stake_flags.rs#L12) - pub const StakeFlags = enum(u8) { - empty = 0, - _, - - pub fn initRandom(random: std.Random) StakeFlags { - return @enumFromInt(random.int(u8)); - } - }; + pub fn fromType(comptime T: type) ?StakesDelegationElement { + return switch (T) { + Delegation => .delegation, + Stake => .stake, + }; + } - pub fn initRandom(random: std.Random) StakeStateV2 { - return switch (random.enumValue(@typeInfo(StakeStateV2).Union.tag_type.?)) { - inline .uninitialized, .rewards_pool => |tag| tag, - .initialized => .{ .initialized = Meta.initRandom(random) }, - .stake => .{ .stake = .{ - Meta.initRandom(random), - Stake.initRandom(random), - StakeFlags.initRandom(random), - } }, + pub fn Type(comptime kind: StakesDelegationElement) type { + return switch (kind) { + .delegation => Delegation, + .stake => Stake, }; } }; -/// Analogous to [Stakes](https://github.com/anza-xyz/agave/blob/1f3ef3325fb0ce08333715aa9d92f831adc4c559/runtime/src/stakes.rs#L186) -pub fn Stakes(comptime StakeDelegationElem: type) type { +/// Analogous to [Stakes](https://github.com/anza-xyz/agave/blob/1f3ef3325fb0ce08333715aa9d92f831adc4c559/runtime/src/stakes.rs#L186). +/// It differs in that its delegation element parameterization is narrowed to only accept the specific types we actually need to implement. +pub fn Stakes(comptime delegation_elem: StakesDelegationElement) type { return struct { /// vote accounts vote_accounts: VoteAccounts, /// stake_delegations - stake_delegations: StakeDelegations, + delegations: DelegationsMap, /// unused unused: u64, /// current epoch, used to calculate current stake epoch: Epoch, /// history of staking levels - stake_history: StakeHistory, + history: EpochAndStakeHistory, const Self = @This(); - pub const StakeDelegations = std.AutoArrayHashMapUnmanaged(Pubkey, StakeDelegationElem); + pub const DelegationElem = delegation_elem.Type(); + pub const DelegationsMap = std.AutoArrayHashMapUnmanaged(Pubkey, DelegationElem); pub fn deinit(stakes: Self, allocator: std.mem.Allocator) void { stakes.vote_accounts.deinit(allocator); + freeDelegations(allocator, stakes.delegations); + allocator.free(stakes.history); + } - var stake_delegations = stakes.stake_delegations; - stake_delegations.deinit(allocator); + fn freeDelegations(allocator: std.mem.Allocator, delegations: DelegationsMap) void { + var copy = delegations; + switch (delegation_elem) { + .delegation, .stake => {}, // these values needn't be deinitialized + } + copy.deinit(allocator); + } + + pub fn clone( + stakes: Self, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!Self { + const vote_accounts = try stakes.vote_accounts.clone(allocator); + errdefer vote_accounts.deinit(allocator); - allocator.free(stakes.stake_history); + const delegations = switch (delegation_elem) { + // these values are trivially copyable + .delegation, .stake => try stakes.delegations.clone(allocator), + }; + errdefer freeDelegations(allocator, delegations); + + const history = try allocator.dupe(EpochAndStakeHistoryEntry, stakes.history); + errdefer allocator.free(history); + + return .{ + .vote_accounts = vote_accounts, + .delegations = delegations, + .unused = stakes.unused, + .epoch = stakes.epoch, + .history = history, + }; } pub fn initRandom( @@ -774,43 +989,34 @@ pub fn Stakes(comptime StakeDelegationElem: type) type { /// for commentary on the runtime of this function. random: std.Random, max_list_entries: usize, - /// Expected to provide methods & fields/decls: - /// * `fn randomValue(delegation_ctx, random: std.Random) StakeDelegationElem`. - /// - /// Also see `sig.rand.fillHashmapWithRng`. - delegation_ctx: anytype, ) std.mem.Allocator.Error!Self { const vote_accounts = try VoteAccounts.initRandom(random, allocator, max_list_entries); errdefer vote_accounts.deinit(allocator); - var stake_delegations = StakeDelegations.Managed.init(allocator); - errdefer stake_delegations.deinit(); + var delegations: DelegationsMap = .{}; + errdefer freeDelegations(allocator, delegations); - try sig.rand.fillHashmapWithRng( - &stake_delegations, - random, - random.uintAtMost(usize, max_list_entries), - struct { - pub fn randomKey(_: @This(), rand: std.Random) !Pubkey { - return Pubkey.initRandom(rand); - } - pub fn randomValue(ctx: @This(), rand: std.Random) !StakeDelegationElem { - return ctx.delegation_ctx.randomValue(rand); - } + const delegations_count = random.uintAtMost(usize, max_list_entries); + try delegations.ensureTotalCapacity(allocator, delegations_count); - delegation_ctx: @TypeOf(delegation_ctx), - }{ .delegation_ctx = delegation_ctx }, - ); + for (0..delegations_count) |_| { + const key = Pubkey.initRandom(random); + const gop = delegations.getOrPutAssumeCapacity(key); + if (gop.found_existing) continue; + gop.value_ptr.* = switch (delegation_elem) { + .delegation, .stake => DelegationElem.initRandom(random), + }; + } - var stake_history = try stakeHistoryRandom(random, allocator, max_list_entries); - errdefer stake_history.deinit(allocator); + const history = try stakeHistoryRandom(random, allocator, max_list_entries); + errdefer history.deinit(allocator); return .{ .vote_accounts = vote_accounts, - .stake_delegations = stake_delegations.unmanaged, + .delegations = delegations, .unused = random.int(u64), .epoch = random.int(Epoch), - .stake_history = stake_history, + .history = history, }; } }; @@ -827,7 +1033,7 @@ pub const VersionedEpochStake = union(enum(u32)) { } pub const Current = struct { - stakes: Stakes(Stake), + stakes: Stakes(.stake), total_stake: u64, node_id_to_vote_accounts: NodeIdToVoteAccountsMap, epoch_authorized_voters: EpochAuthorizedVoters, @@ -844,16 +1050,7 @@ pub const VersionedEpochStake = union(enum(u32)) { random: std.Random, max_list_entries: usize, ) std.mem.Allocator.Error!Current { - const stakes = try Stakes(Stake).initRandom( - allocator, - random, - max_list_entries, - struct { - pub fn randomValue(rand: std.Random) !Stake { - return Stake.initRandom(rand); - } - }, - ); + const stakes = try Stakes(.stake).initRandom(allocator, random, max_list_entries); errdefer stakes.deinit(allocator); const node_id_to_vote_accounts = try nodeIdToVoteAccountsMapRandom( @@ -901,6 +1098,22 @@ pub fn epochStakeMapDeinit( copy.deinit(allocator); } +pub fn epochStakeMapClone( + epoch_stakes: EpochStakeMap, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!EpochStakeMap { + var cloned: EpochStakeMap = .{}; + errdefer epochStakeMapDeinit(cloned, allocator); + try cloned.ensureTotalCapacity(allocator, epoch_stakes.count()); + + for (epoch_stakes.keys(), epoch_stakes.values()) |key, value| { + const cloned_value = try value.clone(allocator); + cloned.putAssumeCapacityNoClobber(key, cloned_value); + } + + return cloned; +} + pub fn epochStakeMapRandom( random: std.Random, allocator: std.mem.Allocator, @@ -959,12 +1172,15 @@ pub const BankFields = struct { rent_collector: RentCollector, epoch_schedule: EpochSchedule, inflation: Inflation, - stakes: Stakes(Delegation), - unused_accounts: UnusedAccounts, // required for deserialization + stakes: Stakes(.delegation), + unused_accounts: UnusedAccounts, epoch_stakes: EpochStakeMap, is_delta: bool, - pub fn deinit(bank_fields: *const BankFields, allocator: std.mem.Allocator) void { + pub fn deinit( + bank_fields: *const BankFields, + allocator: std.mem.Allocator, + ) void { bank_fields.blockhash_queue.deinit(allocator); var ancestors = bank_fields.ancestors; @@ -979,6 +1195,71 @@ pub const BankFields = struct { epochStakeMapDeinit(bank_fields.epoch_stakes, allocator); } + pub fn clone( + bank_fields: *const BankFields, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!BankFields { + const blockhash_queue = try bank_fields.blockhash_queue.clone(allocator); + errdefer blockhash_queue.deinit(allocator); + + var ancestors = try bank_fields.ancestors.clone(allocator); + errdefer ancestors.deinit(allocator); + + const hard_forks = try bank_fields.hard_forks.clone(allocator); + errdefer hard_forks.deinit(allocator); + + const stakes = try bank_fields.stakes.clone(allocator); + errdefer stakes.deinit(allocator); + + const unused_accounts = try bank_fields.unused_accounts.clone(allocator); + errdefer unused_accounts.deinit(allocator); + + const epoch_stakes = try epochStakeMapClone(bank_fields.epoch_stakes, allocator); + errdefer epochStakeMapDeinit(epoch_stakes, allocator); + + var cloned = bank_fields.*; + cloned.blockhash_queue = blockhash_queue; + cloned.ancestors = ancestors; + cloned.hard_forks = hard_forks; + cloned.stakes = stakes; + cloned.unused_accounts = unused_accounts; + cloned.epoch_stakes = epoch_stakes; + return cloned; + } + + pub fn getStakedNodes(self: *const BankFields, allocator: std.mem.Allocator, epoch: Epoch) !*const std.AutoArrayHashMapUnmanaged(Pubkey, u64) { + const epoch_stakes = self.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; + return epoch_stakes.stakes.vote_accounts.stakedNodes(allocator); + } + + /// Returns the leader schedule for this bank's epoch + pub fn leaderSchedule( + self: *const BankFields, + allocator: std.mem.Allocator, + ) !sig.core.leader_schedule.LeaderSchedule { + return self.leaderScheduleForEpoch(allocator, self.epoch); + } + + /// Returns the leader schedule for an arbitrary epoch. + /// Only works if the bank is aware of the staked nodes for that epoch. + pub fn leaderScheduleForEpoch( + self: *const BankFields, + allocator: std.mem.Allocator, + epoch: Epoch, + ) !sig.core.leader_schedule.LeaderSchedule { + const slots_in_epoch = self.epoch_schedule.getSlotsInEpoch(self.epoch); + const staked_nodes = try self.getStakedNodes(allocator, epoch); + return .{ + .allocator = allocator, + .slot_leaders = try sig.core.leader_schedule.LeaderSchedule.fromStakedNodes( + allocator, + epoch, + slots_in_epoch, + staked_nodes, + ), + }; + } + pub fn initRandom( allocator: std.mem.Allocator, /// Should be a PRNG, not a true RNG. See the documentation on `std.Random.uintLessThan` @@ -995,11 +1276,7 @@ pub const BankFields = struct { const hard_forks = try HardForks.initRandom(random, allocator, max_list_entries); errdefer hard_forks.deinit(allocator); - const stakes = try Stakes(Delegation).initRandom(allocator, random, max_list_entries, struct { - pub fn randomValue(rand: std.Random) !Delegation { - return Delegation.initRandom(rand); - } - }); + const stakes = try Stakes(.delegation).initRandom(allocator, random, max_list_entries); errdefer stakes.deinit(allocator); const unused_accounts = try UnusedAccounts.initRandom(random, allocator, max_list_entries); @@ -1043,39 +1320,6 @@ pub const BankFields = struct { .is_delta = random.boolean(), }; } - - pub fn getStakedNodes(self: *const BankFields, allocator: std.mem.Allocator, epoch: Epoch) !*const std.AutoArrayHashMapUnmanaged(Pubkey, u64) { - const epoch_stakes = self.epoch_stakes.getPtr(epoch) orelse return error.NoEpochStakes; - return epoch_stakes.stakes.vote_accounts.stakedNodes(allocator); - } - - /// Returns the leader schedule for this bank's epoch - pub fn leaderSchedule( - self: *const BankFields, - allocator: std.mem.Allocator, - ) !sig.core.leader_schedule.LeaderSchedule { - return self.leaderScheduleForEpoch(allocator, self.epoch); - } - - /// Returns the leader schedule for an arbitrary epoch. - /// Only works if the bank is aware of the staked nodes for that epoch. - pub fn leaderScheduleForEpoch( - self: *const BankFields, - allocator: std.mem.Allocator, - epoch: Epoch, - ) !sig.core.leader_schedule.LeaderSchedule { - const slots_in_epoch = self.epoch_schedule.getSlotsInEpoch(self.epoch); - const staked_nodes = try self.getStakedNodes(allocator, epoch); - return .{ - .allocator = allocator, - .slot_leaders = try sig.core.leader_schedule.LeaderSchedule.fromStakedNodes( - allocator, - epoch, - slots_in_epoch, - staked_nodes, - ), - }; - } }; /// Analogous to [ExtraFieldsToDeserialize](https://github.com/anza-xyz/agave/blob/8d1ef48c785a5d9ee5c0df71dc520ee1a49d8168/runtime/src/serde_snapshot.rs#L396). @@ -1086,6 +1330,18 @@ pub const ExtraFields = struct { versioned_epoch_stakes: VersionedEpochStakesMap, accounts_lt_hash: ?AccountsLtHash, + pub const @"!bincode-config": bincode.FieldConfig(ExtraFields) = .{ + .deserializer = bincodeRead, + .serializer = null, // just use default serialization method + .free = bincodeFree, + }; + + pub const VersionedEpochStakesMap = std.AutoArrayHashMapUnmanaged(u64, VersionedEpochStake); + + /// TODO: https://github.com/orgs/Syndica/projects/2/views/10?pane=issue&itemId=85238686 + pub const ACCOUNTS_LATTICE_HASH_LEN = 1024; + pub const AccountsLtHash = [ACCOUNTS_LATTICE_HASH_LEN]u16; + pub const INIT_EOF: ExtraFields = .{ .lamports_per_signature = 0, .snapshot_persistence = null, @@ -1100,17 +1356,24 @@ pub const ExtraFields = struct { versioned_epoch_stakes.deinit(allocator); } - pub const VersionedEpochStakesMap = std.AutoArrayHashMapUnmanaged(u64, VersionedEpochStake); - - /// TODO: https://github.com/orgs/Syndica/projects/2/views/10?pane=issue&itemId=85238686 - pub const ACCOUNTS_LATTICE_HASH_LEN = 1024; - pub const AccountsLtHash = [ACCOUNTS_LATTICE_HASH_LEN]u16; + pub fn clone( + extra: *const ExtraFields, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!ExtraFields { + var versioned_epoch_stakes: VersionedEpochStakesMap = .{}; + errdefer versioned_epoch_stakes.deinit(allocator); + errdefer for (extra.versioned_epoch_stakes.values()) |ves| { + ves.deinit(allocator); + }; - pub const @"!bincode-config": bincode.FieldConfig(ExtraFields) = .{ - .deserializer = bincodeRead, - .serializer = null, // just use default serialization method - .free = bincodeFree, - }; + return .{ + .lamports_per_signature = extra.lamports_per_signature, + .snapshot_persistence = extra.snapshot_persistence, + .epoch_accounts_hash = extra.epoch_accounts_hash, + .versioned_epoch_stakes = versioned_epoch_stakes, + .accounts_lt_hash = extra.accounts_lt_hash, + }; + } pub fn initRandom( allocator: std.mem.Allocator, @@ -1344,6 +1607,12 @@ pub const AccountsDbFields = struct { rooted_slots: []const Slot, rooted_slot_hashes: []const SlotAndHash, + pub const @"!bincode-config": bincode.FieldConfig(AccountsDbFields) = .{ + .deserializer = bincodeRead, + .serializer = bincodeWrite, + .free = bincodeFree, + }; + pub const FileMap = std.AutoArrayHashMapUnmanaged(Slot, AccountFileInfo); pub fn deinit(fields: AccountsDbFields, allocator: std.mem.Allocator) void { @@ -1354,11 +1623,28 @@ pub const AccountsDbFields = struct { allocator.free(fields.rooted_slot_hashes); } - pub const @"!bincode-config": bincode.FieldConfig(AccountsDbFields) = .{ - .deserializer = bincodeRead, - .serializer = bincodeWrite, - .free = bincodeFree, - }; + pub fn clone( + fields: AccountsDbFields, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!AccountsDbFields { + var file_map = try fields.file_map.clone(allocator); + errdefer file_map.deinit(allocator); + + const rooted_slots = try allocator.dupe(Slot, fields.rooted_slots); + errdefer allocator.free(rooted_slots); + + const rooted_slot_hashes = try allocator.dupe(SlotAndHash, fields.rooted_slot_hashes); + errdefer allocator.free(rooted_slot_hashes); + + return .{ + .file_map = file_map, + .stored_meta_write_version = fields.stored_meta_write_version, + .slot = fields.slot, + .bank_hash_info = fields.bank_hash_info, + .rooted_slots = rooted_slots, + .rooted_slot_hashes = rooted_slot_hashes, + }; + } fn bincodeRead( allocator: std.mem.Allocator, @@ -1452,22 +1738,42 @@ pub const AccountsDbFields = struct { /// contains all the metadata from a snapshot. /// this includes fields for accounts-db and the bank of the snapshots slots. /// this does not include account-specific data. -pub const SnapshotFields = struct { +pub const Manifest = struct { bank_fields: BankFields, accounts_db_fields: AccountsDbFields, /// incremental snapshot fields. bank_extra: ExtraFields, - pub fn deinit(self: SnapshotFields, allocator: std.mem.Allocator) void { + pub fn deinit(self: Manifest, allocator: std.mem.Allocator) void { self.bank_fields.deinit(allocator); self.accounts_db_fields.deinit(allocator); self.bank_extra.deinit(allocator); } + pub fn clone( + man: Manifest, + allocator: std.mem.Allocator, + ) std.mem.Allocator.Error!Manifest { + const bank_fields = try man.bank_fields.clone(allocator); + errdefer bank_fields.deinit(allocator); + + const accounts_db_fields = try man.accounts_db_fields.clone(allocator); + errdefer accounts_db_fields.deinit(allocator); + + const bank_extra = try man.bank_extra.clone(allocator); + errdefer bank_extra.deinit(allocator); + + return .{ + .bank_fields = bank_fields, + .accounts_db_fields = accounts_db_fields, + .bank_extra = bank_extra, + }; + } + pub fn readFromFilePath( allocator: std.mem.Allocator, path: []const u8, - ) !SnapshotFields { + ) !Manifest { const file = std.fs.cwd().openFile(path, .{}) catch |err| { switch (err) { error.FileNotFound => return error.SnapshotFieldsNotFound, @@ -1481,7 +1787,7 @@ pub const SnapshotFields = struct { pub fn readFromFile( allocator: std.mem.Allocator, file: std.fs.File, - ) !SnapshotFields { + ) !Manifest { const size = (try file.stat()).size; const contents = try file.readToEndAllocOptions(allocator, size, size, @alignOf(u8), null); defer allocator.free(contents); @@ -1494,8 +1800,8 @@ pub const SnapshotFields = struct { allocator: std.mem.Allocator, /// `std.io.GenericReader(...)` | `std.io.AnyReader` reader: anytype, - ) !SnapshotFields { - return try bincode.read(allocator, SnapshotFields, reader, .{}); + ) !Manifest { + return try bincode.read(allocator, Manifest, reader, .{}); } }; @@ -2287,55 +2593,46 @@ pub const SnapshotFiles = struct { } }; -/// contains all fields from a snapshot (full and incremental) +/// Represents the full manifest optionally combined with an incremental manifest. /// /// Analogous to [SnapshotBankFields](https://github.com/anza-xyz/agave/blob/2de7b565e8b1101824a5e3bac74f3a8cce88ea72/runtime/src/serde_snapshot.rs#L299) -pub const AllSnapshotFields = struct { - full: SnapshotFields, - incremental: ?SnapshotFields, - was_collapsed: bool = false, // used for deinit() - - const Self = @This(); +pub const FullAndIncrementalManifest = struct { + full: Manifest, + incremental: ?Manifest, pub fn fromFiles( allocator: std.mem.Allocator, - logger_: Logger, + unscoped_logger: Logger, snapshot_dir: std.fs.Dir, files: SnapshotFiles, - ) !Self { - const logger = logger_.withScope(@typeName((Self))); - // unpack + ) !FullAndIncrementalManifest { + const logger = unscoped_logger.withScope("accounts_db.snapshot_manifest"); + const full_fields = blk: { const rel_path_bounded = sig.utils.fmt.boundedFmt("snapshots/{0}/{0}", .{files.full.slot}); const rel_path = rel_path_bounded.constSlice(); - logger.info().logf("reading snapshot fields from: {s}", .{sig.utils.fmt.tryRealPath(snapshot_dir, rel_path)}); + logger.info().logf("reading *full* snapshot fields from: {s}", .{sig.utils.fmt.tryRealPath(snapshot_dir, rel_path)}); const full_file = try snapshot_dir.openFile(rel_path, .{}); defer full_file.close(); - break :blk try SnapshotFields.readFromFile(allocator, full_file); + break :blk try Manifest.readFromFile(allocator, full_file); }; errdefer full_fields.deinit(allocator); - const incremental_fields: ?SnapshotFields = blk: { - if (files.incremental_info) |inc_snap| { - const rel_path_bounded = sig.utils.fmt.boundedFmt("snapshots/{0}/{0}", .{inc_snap.slot}); - const rel_path = rel_path_bounded.constSlice(); - - logger.info().logf("reading inc snapshot fields from: {s}", .{sig.utils.fmt.tryRealPath(snapshot_dir, rel_path)}); - - const incremental_file = try snapshot_dir.openFile(rel_path, .{}); - defer incremental_file.close(); + const incremental_fields = if (files.incremental_info) |inc_snap| blk: { + const rel_path_bounded = sig.utils.fmt.boundedFmt("snapshots/{0}/{0}", .{inc_snap.slot}); + const rel_path = rel_path_bounded.constSlice(); + logger.info().logf("reading *incremental* snapshot fields from: {s}", .{sig.utils.fmt.tryRealPath(snapshot_dir, rel_path)}); - const incremental_fields = try SnapshotFields.readFromFile(allocator, incremental_file); - errdefer incremental_fields.deinit(allocator); + const incremental_file = try snapshot_dir.openFile(rel_path, .{}); + defer incremental_file.close(); - break :blk incremental_fields; - } else { - logger.info().log("no incremental snapshot fields found"); - break :blk null; - } + break :blk try Manifest.readFromFile(allocator, incremental_file); + } else blk: { + logger.info().log("no incremental snapshot fields found"); + break :blk null; }; errdefer if (incremental_fields) |fields| fields.deinit(allocator); @@ -2345,78 +2642,104 @@ pub const AllSnapshotFields = struct { }; } - /// collapse all full and incremental snapshots into one. - /// note: this works by stack copying the full snapshot and combining - /// the accounts-db account file map. - /// this will 1) modify the incremental snapshot account map - /// and 2) the returned snapshot heap fields will still point to the incremental snapshot - /// (so be sure not to deinit it while still using the returned snapshot) + pub const CollapseError = error{ + /// There are storages for the same slot in both the full and incremental snapshot. + SnapshotSlotOverlap, + }; + + /// Like `collapseIfNecessary`, but returns a clone of the full snapshot + /// manifest if there is no incremental update to apply. + /// The caller is responsible for `.deinit`ing the result with `allocator`. pub fn collapse( - self: *Self, - /// Should be the same allocator passed to `fromFiles`, or otherwise to allocate `Self`. + self: FullAndIncrementalManifest, allocator: std.mem.Allocator, - ) !SnapshotFields { - // nothing to collapse - if (self.incremental == null) - return self.full; - self.was_collapsed = true; - - // collapse bank fields into the - // incremental =pushed into=> full - var snapshot = self.incremental.?; // stack copy - const full_slot = self.full.bank_fields.slot; - - // collapse accounts-db fields - const storages_map = &self.incremental.?.accounts_db_fields.file_map; - - // TODO: use a better allocator - var slots_to_remove = std.ArrayList(Slot).init(allocator); - defer slots_to_remove.deinit(); - - // make sure theres no overlap in slots between full and incremental and combine - var storages_entry_iter = storages_map.iterator(); - while (storages_entry_iter.next()) |incremental_entry| { - const slot = incremental_entry.key_ptr.*; - - // only keep slots > full snapshot slot - if (!(slot > full_slot)) { - try slots_to_remove.append(slot); - continue; - } + ) (std.mem.Allocator.Error || CollapseError)!Manifest { + const maybe_collapsed = try self.collapseIfNecessary(allocator); + return maybe_collapsed orelse try self.full.clone(allocator); + } + + /// Returns null if there is no incremental snapshot manifest; otherwise + /// returns the result of overlaying the updates of the incremental + /// onto the full snapshot manifest. + /// The caller is responsible for `.deinit`ing the result with `allocator` + /// if it is non-null. + pub fn collapseIfNecessary( + self: FullAndIncrementalManifest, + allocator: std.mem.Allocator, + ) (std.mem.Allocator.Error || CollapseError)!?Manifest { + const full = self.full; + const incremental = self.incremental orelse return null; - const slot_entry = try self.full.accounts_db_fields.file_map.getOrPut(allocator, slot); - if (slot_entry.found_existing) { - std.debug.panic("invalid incremental snapshot: slot {d} is in both full and incremental snapshots\n", .{slot}); - } else { - slot_entry.value_ptr.* = incremental_entry.value_ptr.*; - } - } + // make a heap clone of the incremental manifest's more up-to-date + // data, except with the file map of the full manifest, which is + // likely to contain a larger amount of entries; can then overlay + // the relevant entries from the incremental manifest onto the + // clone of the full manifest. - for (slots_to_remove.items) |slot| { - _ = storages_map.swapRemove(slot); - } + var collapsed = incremental; + collapsed.accounts_db_fields.file_map = full.accounts_db_fields.file_map; + + collapsed = try collapsed.clone(allocator); + errdefer collapsed.deinit(allocator); + + const collapsed_file_map = &collapsed.accounts_db_fields.file_map; + try collapsed_file_map.ensureUnusedCapacity( + allocator, + incremental.accounts_db_fields.file_map.count(), + ); - snapshot.accounts_db_fields = self.full.accounts_db_fields; + const inc_file_map = &incremental.accounts_db_fields.file_map; + for (inc_file_map.keys(), inc_file_map.values()) |slot, account_file_info| { + if (slot <= full.accounts_db_fields.slot) continue; + const gop = collapsed_file_map.getOrPutAssumeCapacity(slot); + if (gop.found_existing) return error.SnapshotSlotOverlap; + gop.value_ptr.* = account_file_info; + } - return snapshot; + return collapsed; } - pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { + pub fn deinit(self: FullAndIncrementalManifest, allocator: std.mem.Allocator) void { self.full.deinit(allocator); - if (self.incremental) |*inc| { - if (!self.was_collapsed) { - inc.deinit(allocator); - } else { - inc.accounts_db_fields.file_map.deinit(allocator); - inc.bank_fields.deinit(allocator); - allocator.free(inc.accounts_db_fields.rooted_slots); - allocator.free(inc.accounts_db_fields.rooted_slot_hashes); - inc.bank_extra.deinit(allocator); - } + if (self.incremental) |inc| { + inc.deinit(allocator); } } }; +test "checkAllAllocationFailures FullAndIncrementalManifest" { + const local = struct { + fn parseFiles( + allocator: std.mem.Allocator, + snapdir: std.fs.Dir, + snapshot_files: SnapshotFiles, + ) !void { + const combined_manifest = try FullAndIncrementalManifest.fromFiles( + allocator, + .noop, + snapdir, + snapshot_files, + ); + defer combined_manifest.deinit(allocator); + + const collapsed_manifest = try combined_manifest.collapse(allocator); + defer collapsed_manifest.deinit(allocator); + } + }; + + var tmp_dir_root = std.testing.tmpDir(.{}); + defer tmp_dir_root.cleanup(); + const snapdir = tmp_dir_root.dir; + + const snapshot_files = try sig.accounts_db.db.findAndUnpackTestSnapshots(1, snapdir); + + try std.testing.checkAllAllocationFailures( + std.testing.allocator, + local.parseFiles, + .{ snapdir, snapshot_files }, + ); +} + pub const generate = struct { /// Writes the version, status cache, and manifest files. /// Should call this first to begin generating the snapshot archive. @@ -2424,7 +2747,7 @@ pub const generate = struct { archive_writer: anytype, version: sig.version.ClientVersion, status_cache: StatusCache, - manifest: *const SnapshotFields, + manifest: *const Manifest, ) !void { const slot: Slot = manifest.bank_fields.slot; @@ -2486,21 +2809,21 @@ pub fn parallelUnpackZstdTarBall( /// only used for progress estimation full_snapshot: bool, ) !void { - const file_stat = try file.stat(); - const file_size: u64 = @intCast(file_stat.size); - const memory = try std.posix.mmap( - null, - file_size, - std.posix.PROT.READ, - std.posix.MAP{ .TYPE = .SHARED }, - file.handle, - 0, - ); - var tar_stream = try zstd.Reader.init(memory); + const file_size = (try file.stat()).size; + + // TODO: improve `zstd.Reader` to be capable of sourcing a stream of bytes + // rather than a fixed slice of bytes, so we don't have to load the entire + // snapshot file into memory. + const file_data = try allocator.alloc(u8, file_size); + defer allocator.free(file_data); + if (try file.readAll(file_data) != file_size) { + return error.UnexpectedEOF; // has the file shrunk since we got its size? + } + var tar_stream = try zstd.Reader.init(file_data); defer tar_stream.deinit(); const n_files_estimate: usize = if (full_snapshot) 421_764 else 100_000; // estimate - try parallelUntarToFileSystem( + try sig.utils.tar.parallelUntarToFileSystem( allocator, logger, output_dir, @@ -2575,8 +2898,8 @@ test "parse snapshot fields" { const full_manifest_file = try snapdir.openFile(full_manifest_path, .{}); defer full_manifest_file.close(); - const snapshot_fields_full = try SnapshotFields.readFromFile(allocator, full_manifest_file); - defer snapshot_fields_full.deinit(allocator); + const full_manifest = try Manifest.readFromFile(allocator, full_manifest_file); + defer full_manifest.deinit(allocator); if (snapshot_files.incremental_info) |inc| { const inc_slot = inc.slot; @@ -2586,7 +2909,7 @@ test "parse snapshot fields" { const inc_manifest_file = try snapdir.openFile(inc_manifest_path, .{}); defer inc_manifest_file.close(); - const snapshot_fields_inc = try SnapshotFields.readFromFile(allocator, inc_manifest_file); - defer snapshot_fields_inc.deinit(allocator); + const inc_manifest = try Manifest.readFromFile(allocator, inc_manifest_file); + defer inc_manifest.deinit(allocator); } } diff --git a/src/benchmarks.zig b/src/benchmarks.zig index 6732a59fa..0d083ba0f 100644 --- a/src/benchmarks.zig +++ b/src/benchmarks.zig @@ -33,7 +33,7 @@ const Benchmark = enum { all, accounts_db, accounts_db_readwrite, - accounts_db_snapshot, + accounts_db_snapshot, // expensive bincode, geyser, gossip, @@ -66,8 +66,13 @@ fn exitWithUsage() noreturn { \\ Prints this usage message \\ \\ --metrics - \\ save benchmark results to results/output.json + \\ save benchmark results to results/output.json. default: false. \\ + \\ -e + \\ run expensive benchmarks. default: false. + \\ + \\ -f + \\ force fresh state for expensive benchmarks. default: false. ) catch @panic("failed to print usage"); std.posix.exit(1); } @@ -87,7 +92,10 @@ pub fn main() !void { var cli_args = try std.process.argsWithAllocator(allocator); defer cli_args.deinit(); + var run_expensive_benchmarks: bool = false; var collect_metrics: bool = false; + var force_fresh_state: bool = false; + var maybe_filter: ?Benchmark = null; // skip the benchmark argv[0] _ = cli_args.skip(); @@ -97,6 +105,13 @@ pub fn main() !void { } else if (std.mem.startsWith(u8, arg, "--metrics")) { collect_metrics = true; continue; + } else if (std.mem.startsWith(u8, arg, "-e")) { + run_expensive_benchmarks = true; + collect_metrics = true; // by default collect metrics when running expensive benchmarks + continue; + } else if (std.mem.startsWith(u8, arg, "-f")) { + force_fresh_state = true; + continue; } maybe_filter = std.meta.stringToEnum(Benchmark, arg) orelse { logger.err().logf("unknown benchmark: {s}", .{arg}); @@ -160,15 +175,70 @@ pub fn main() !void { ); } - if (filter == .accounts_db_snapshot or run_all) blk: { - // NOTE: for this benchmark you need to setup a snapshot in test-data/snapshot_bench - // and run as a binary ./zig-out/bin/... so the open file limits are ok - const dir_path = sig.TEST_DATA_DIR ++ "bench_snapshot/"; - var snapshot_dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch { - logger.debug().logf("[accounts_db_snapshot]: need to setup a snapshot in {s} for this benchmark...", .{dir_path}); - break :blk; - }; - snapshot_dir.close(); + if ((filter == .accounts_db_snapshot or run_all) and !run_expensive_benchmarks) { + logger.warn().log("[accounts_db_snapshot]: skipping benchmark, use -e to run"); + } + + if ((filter == .accounts_db_snapshot or run_all) and run_expensive_benchmarks) { + // NOTE: snapshot must exist in this directory for the benchmark to run + // NOTE: also need to increase file limits to run this benchmark (see debugging.md) + const BENCH_SNAPSHOT_DIR_PATH = @import("accountsdb/db.zig") + .BenchmarkAccountsDBSnapshotLoad + .SNAPSHOT_DIR_PATH; + + var test_snapshot_exists = true; + if (std.fs.cwd().openDir(BENCH_SNAPSHOT_DIR_PATH, .{ .iterate = true })) |dir| { + std.posix.close(dir.fd); + } else |_| { + test_snapshot_exists = false; + } + + const download_new_snapshot = force_fresh_state or !test_snapshot_exists; + if (download_new_snapshot) { + // delete existing snapshot dir + if (test_snapshot_exists) { + std.debug.print("deleting snapshot dir...\n", .{}); + std.fs.cwd().deleteTreeMinStackSize(BENCH_SNAPSHOT_DIR_PATH) catch |err| { + std.debug.print("failed to delete snapshot dir ('{s}'): {}\n", .{ + BENCH_SNAPSHOT_DIR_PATH, + err, + }); + }; + } + + // create fresh snapshot dir + var snapshot_dir = try std.fs.cwd().makeOpenPath( + BENCH_SNAPSHOT_DIR_PATH, + .{ .iterate = true }, + ); + defer snapshot_dir.close(); + + // start gossip + const gossip_service = try sig.gossip.helpers.initGossipFromCluster( + allocator, + logger.unscoped(), + .testnet, // TODO: support other clusters + ); + defer { + gossip_service.shutdown(); + gossip_service.deinit(); + allocator.destroy(gossip_service); + } + try gossip_service.start(.{}); + + // download and unpack snapshot + var snapshot_manifests, _ = try sig.accounts_db.download.getOrDownloadAndUnpackSnapshot( + allocator, + logger, + snapshot_dir, + null, + .{ + .gossip_service = gossip_service, + .force_new_snapshot_download = true, + }, + ); + defer snapshot_manifests.deinit(allocator); + } try benchmark( allocator, diff --git a/src/bincode/hashmap.zig b/src/bincode/hashmap.zig index e335c9f1a..db81cf543 100644 --- a/src/bincode/hashmap.zig +++ b/src/bincode/hashmap.zig @@ -101,11 +101,10 @@ pub fn readCtx( const key = try ctx_impl.readKey(allocator, reader, params); errdefer ctx_impl.freeKey(allocator, key); - const gop = hash_map.getOrPutAssumeCapacity(key); - if (gop.found_existing) return error.DuplicateFileMapEntry; + if (hash_map.contains(key)) return error.DuplicateFileMapEntry; const value = try ctx_impl.readValue(allocator, reader, params); - gop.value_ptr.* = value; + hash_map.putAssumeCapacityNoClobber(key, value); } return switch (hm_info.management) { diff --git a/src/cmd/cmd.zig b/src/cmd/cmd.zig index 209a124e7..d412b94f4 100644 --- a/src/cmd/cmd.zig +++ b/src/cmd/cmd.zig @@ -10,7 +10,7 @@ const zstd = @import("zstd"); const Allocator = std.mem.Allocator; const KeyPair = std.crypto.sign.Ed25519.KeyPair; const AccountsDB = sig.accounts_db.AccountsDB; -const AllSnapshotFields = sig.accounts_db.AllSnapshotFields; +const FullAndIncrementalManifest = sig.accounts_db.FullAndIncrementalManifest; const Bank = sig.accounts_db.Bank; const Slot = sig.core.Slot; const ContactInfo = sig.gossip.ContactInfo; @@ -37,7 +37,6 @@ const getOrInitIdentity = sig.cmd.helpers.getOrInitIdentity; const downloadSnapshotsFromGossip = sig.accounts_db.downloadSnapshotsFromGossip; const globalRegistry = sig.prometheus.globalRegistry; const getWallclockMs = sig.time.getWallclockMs; -const parallelUnpackZstdTarBall = sig.accounts_db.parallelUnpackZstdTarBall; const spawnMetrics = sig.prometheus.spawnMetrics; const getShredAndIPFromEchoServer = sig.net.echo.getShredAndIPFromEchoServer; const createGeyserWriterFromConfig = sig.geyser.core.createGeyserWriterFromConfig; @@ -750,23 +749,25 @@ fn validator() !void { } // snapshot - const snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ + var loaded_snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ .gossip_service = gossip_service, .geyser_writer = geyser_writer, .validate_snapshot = true, }); + defer loaded_snapshot.deinit(); // leader schedule - var leader_schedule_cache = LeaderScheduleCache.init(allocator, snapshot.bank.bank_fields.epoch_schedule); + var leader_schedule_cache = LeaderScheduleCache.init(allocator, loaded_snapshot.collapsed_manifest.bank_fields.epoch_schedule); if (try getLeaderScheduleFromCli(allocator)) |leader_schedule| { - try leader_schedule_cache.put(snapshot.bank.bank_fields.epoch, leader_schedule[1]); + try leader_schedule_cache.put(loaded_snapshot.collapsed_manifest.bank_fields.epoch, leader_schedule[1]); } else { - const schedule = try snapshot.bank.bank_fields.leaderSchedule(allocator); - try leader_schedule_cache.put(snapshot.bank.bank_fields.epoch, schedule); + const schedule = try loaded_snapshot.collapsed_manifest.bank_fields.leaderSchedule(allocator); + errdefer schedule.deinit(); + try leader_schedule_cache.put(loaded_snapshot.collapsed_manifest.bank_fields.epoch, schedule); } // This provider will fail at epoch boundary unless another thread updated the leader schedule cache // i.e. called leader_schedule_cache.getSlotLeaderMaybeCompute(slot, bank_fields); - const leader_provider = leader_schedule_cache.slotLeaderProvider(); + const leader_provider = leader_schedule_cache.slotLeaders(); // blockstore var blockstore_db = try sig.ledger.BlockstoreDB.open( @@ -817,7 +818,7 @@ fn validator() !void { // shred networking const my_contact_info = sig.gossip.data.ThreadSafeContactInfo.fromContactInfo(gossip_service.my_contact_info); var shred_col_conf = config.current.shred_network; - shred_col_conf.start_slot = shred_col_conf.start_slot orelse snapshot.bank.bank_fields.slot; + shred_col_conf.start_slot = shred_col_conf.start_slot orelse loaded_snapshot.collapsed_manifest.bank_fields.slot; var shred_network_manager = try sig.shred_network.start( shred_col_conf, ShredCollectorDependencies{ @@ -835,7 +836,7 @@ fn validator() !void { .n_retransmit_threads = config.current.turbine.num_retransmit_threads, .overwrite_turbine_stake_for_testing = config.current.turbine.overwrite_stake_for_testing, .leader_schedule_cache = &leader_schedule_cache, - .bank_fields = snapshot.bank.bank_fields, + .bank_fields = &loaded_snapshot.collapsed_manifest.bank_fields, }, ); defer shred_network_manager.deinit(); @@ -865,24 +866,26 @@ fn shredCollector() !void { allocator.destroy(gossip_service); } - const snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ + var loaded_snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ .gossip_service = gossip_service, .geyser_writer = null, .validate_snapshot = true, .metadata_only = config.current.accounts_db.snapshot_metadata_only, }); + defer loaded_snapshot.deinit(); // leader schedule - var leader_schedule_cache = LeaderScheduleCache.init(allocator, snapshot.bank.bank_fields.epoch_schedule); + var leader_schedule_cache = LeaderScheduleCache.init(allocator, loaded_snapshot.collapsed_manifest.bank_fields.epoch_schedule); if (try getLeaderScheduleFromCli(allocator)) |leader_schedule| { - try leader_schedule_cache.put(snapshot.bank.bank_fields.epoch, leader_schedule[1]); + try leader_schedule_cache.put(loaded_snapshot.collapsed_manifest.bank_fields.epoch, leader_schedule[1]); } else { - const schedule = try snapshot.bank.bank_fields.leaderSchedule(allocator); - try leader_schedule_cache.put(snapshot.bank.bank_fields.epoch, schedule); + const schedule = try loaded_snapshot.collapsed_manifest.bank_fields.leaderSchedule(allocator); + errdefer schedule.deinit(); + try leader_schedule_cache.put(loaded_snapshot.collapsed_manifest.bank_fields.epoch, schedule); } // This provider will fail at epoch boundary unless another thread updated the leader schedule cache // i.e. called leader_schedule_cache.getSlotLeaderMaybeCompute(slot, bank_fields); - const leader_provider = leader_schedule_cache.slotLeaderProvider(); + const leader_provider = leader_schedule_cache.slotLeaders(); // blockstore var blockstore_db = try sig.ledger.BlockstoreDB.open( @@ -951,7 +954,7 @@ fn shredCollector() !void { .n_retransmit_threads = config.current.turbine.num_retransmit_threads, .overwrite_turbine_stake_for_testing = config.current.turbine.overwrite_stake_for_testing, .leader_schedule_cache = &leader_schedule_cache, - .bank_fields = snapshot.bank.bank_fields, + .bank_fields = &loaded_snapshot.collapsed_manifest.bank_fields, }, ); defer shred_network_manager.deinit(); @@ -974,7 +977,7 @@ fn printManifest() !void { const snapshot_file_info = try SnapshotFiles.find(allocator, snapshot_dir); - var snapshots = try AllSnapshotFields.fromFiles( + var snapshots = try FullAndIncrementalManifest.fromFiles( allocator, app_base.logger.unscoped(), snapshot_dir, @@ -1000,16 +1003,16 @@ fn createSnapshot() !void { var snapshot_dir = try std.fs.cwd().makeOpenPath(snapshot_dir_str, .{}); defer snapshot_dir.close(); - const snapshot_result = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ + var loaded_snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ .gossip_service = null, .geyser_writer = null, .validate_snapshot = false, .metadata_only = false, }); - defer snapshot_result.deinit(); + defer loaded_snapshot.deinit(); - var accounts_db = snapshot_result.accounts_db; - const slot = snapshot_result.snapshot_fields.full.bank_fields.slot; + var accounts_db = loaded_snapshot.accounts_db; + const slot = loaded_snapshot.combined_manifest.full.bank_fields.slot; var n_accounts_indexed: u64 = 0; for (accounts_db.account_index.pubkey_ref_map.shards) |*shard_rw| { @@ -1026,7 +1029,7 @@ fn createSnapshot() !void { app_base.logger.info().logf("accountsdb[manager]: generating full snapshot for slot {d}", .{slot}); _ = try accounts_db.generateFullSnapshot(.{ .target_slot = slot, - .bank_fields = &snapshot_result.snapshot_fields.full.bank_fields, + .bank_fields = &loaded_snapshot.combined_manifest.full.bank_fields, .lamports_per_signature = lps: { var prng = std.Random.DefaultPrng.init(1234); break :lps prng.random().int(u64); @@ -1061,13 +1064,13 @@ fn validateSnapshot() !void { } } - const snapshot_result = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ + var loaded_snapshot = try loadSnapshot(allocator, app_base.logger.unscoped(), .{ .gossip_service = null, .geyser_writer = geyser_writer, .validate_snapshot = true, .metadata_only = false, }); - defer snapshot_result.deinit(); + defer loaded_snapshot.deinit(); } /// entrypoint to print the leader schedule and then exit @@ -1081,7 +1084,8 @@ fn printLeaderSchedule() !void { const start_slot, const leader_schedule = try getLeaderScheduleFromCli(allocator) orelse b: { app_base.logger.info().log("Downloading a snapshot to calculate the leader schedule."); - const loaded_snapshot = loadSnapshot(allocator, app_base.logger.unscoped(), .{ + + var loaded_snapshot = loadSnapshot(allocator, app_base.logger.unscoped(), .{ .gossip_service = null, .geyser_writer = null, .validate_snapshot = true, @@ -1092,12 +1096,12 @@ fn printLeaderSchedule() !void { \\\ No snapshot found and no gossip service to download a snapshot from. \\\ Download using the `snapshot-download` command. ); - return err; - } else { - return err; } + return err; }; - const bank_fields = loaded_snapshot.bank.bank_fields; + defer loaded_snapshot.deinit(); + + const bank_fields = &loaded_snapshot.collapsed_manifest.bank_fields; _, const slot_index = bank_fields.epoch_schedule.getEpochAndSlotIndex(bank_fields.slot); break :b .{ bank_fields.slot - slot_index, @@ -1238,8 +1242,8 @@ const AppBase = struct { const my_shred_version = echo_data.shred_version orelse 0; logger.info().logf("my shred version: {d}", .{my_shred_version}); - const my_ip = config.current.gossip.getHost() catch - echo_data.ip orelse IpAddr.newIpv4(127, 0, 0, 1); + const config_host = config.current.gossip.getHost() catch null; + const my_ip = config_host orelse echo_data.ip orelse IpAddr.newIpv4(127, 0, 0, 1); logger.info().logf("my ip: {any}", .{my_ip}); const my_port = config.current.gossip.port; @@ -1318,22 +1322,19 @@ fn spawnLogger(allocator: std.mem.Allocator) !Logger { const LoadedSnapshot = struct { allocator: Allocator, accounts_db: AccountsDB, - status_cache: sig.accounts_db.snapshots.StatusCache, - snapshot_fields: sig.accounts_db.snapshots.AllSnapshotFields, - /// contains pointers to `accounts_db` and `snapshot_fields` - bank: Bank, + combined_manifest: sig.accounts_db.snapshots.FullAndIncrementalManifest, + collapsed_manifest: sig.accounts_db.snapshots.Manifest, genesis_config: GenesisConfig, - // Snapshot resulting from collapse needs to be retained here for - // valid lifetime as it is used by bank. This was a quick fix, a minor - // refactor is probably not a bad idea. - collapsed_snapshot_fields: sig.accounts_db.snapshots.SnapshotFields, + status_cache: ?sig.accounts_db.snapshots.StatusCache, pub fn deinit(self: *@This()) void { - self.genesis_config.deinit(self.allocator); - self.status_cache.deinit(self.allocator); - self.snapshot_fields.deinit(self.allocator); self.accounts_db.deinit(); - self.allocator.destroy(self); + self.combined_manifest.deinit(self.allocator); + self.collapsed_manifest.deinit(self.allocator); + self.genesis_config.deinit(self.allocator); + if (self.status_cache) |status_cache| { + status_cache.deinit(self.allocator); + } } }; @@ -1350,13 +1351,13 @@ const LoadSnapshotOptions = struct { fn loadSnapshot( allocator: Allocator, - logger_: Logger, + unscoped_logger: Logger, options: LoadSnapshotOptions, -) !*LoadedSnapshot { - const logger = logger_.withScope(@typeName(@This())); - const result = try allocator.create(LoadedSnapshot); - errdefer allocator.destroy(result); - result.allocator = allocator; +) !LoadedSnapshot { + const logger = unscoped_logger.withScope(@typeName(@This()) ++ "." ++ @src().fn_name); + + var validator_dir = try std.fs.cwd().openDir(sig.VALIDATOR_DIR, .{}); + defer validator_dir.close(); const genesis_file_path = try config.current.genesisFilePath() orelse return error.GenesisPathNotProvided; @@ -1365,14 +1366,19 @@ fn loadSnapshot( var snapshot_dir = try std.fs.cwd().makeOpenPath(snapshot_dir_str, .{ .iterate = true }); defer snapshot_dir.close(); - var all_snapshot_fields, const snapshot_files = try getOrDownloadSnapshots(allocator, logger.unscoped(), options.gossip_service, .{ - .snapshot_dir = snapshot_dir, - .force_unpack_snapshot = config.current.accounts_db.force_unpack_snapshot, - .force_new_snapshot_download = config.current.accounts_db.force_new_snapshot_download, - .num_threads_snapshot_unpack = config.current.accounts_db.num_threads_snapshot_unpack, - .min_snapshot_download_speed_mbs = config.current.accounts_db.min_snapshot_download_speed_mbs, - }); - result.snapshot_fields = all_snapshot_fields; + const combined_manifest, const snapshot_files = try sig.accounts_db.download.getOrDownloadAndUnpackSnapshot( + allocator, + logger.unscoped(), + validator_dir, + snapshot_dir, + .{ + .gossip_service = options.gossip_service, + .force_unpack_snapshot = config.current.accounts_db.force_unpack_snapshot, + .force_new_snapshot_download = config.current.accounts_db.force_new_snapshot_download, + .num_threads_snapshot_unpack = config.current.accounts_db.num_threads_snapshot_unpack, + .min_snapshot_download_speed_mbs = config.current.accounts_db.min_snapshot_download_speed_mbs, + }, + ); logger.info().logf("full snapshot: {s}", .{ sig.utils.fmt.tryRealPath(snapshot_dir, snapshot_files.full.snapshotArchiveName().constSlice()), @@ -1395,7 +1401,7 @@ fn loadSnapshot( }; logger.info().logf("n_threads_snapshot_load: {d}", .{n_threads_snapshot_load}); - result.accounts_db = try AccountsDB.init(.{ + var accounts_db = try AccountsDB.init(.{ .allocator = allocator, .logger = logger.unscoped(), .snapshot_dir = snapshot_dir, @@ -1405,45 +1411,51 @@ fn loadSnapshot( .number_of_index_shards = config.current.accounts_db.number_of_index_shards, .lru_size = 10_000, }); - errdefer result.accounts_db.deinit(); + errdefer accounts_db.deinit(); - if (options.metadata_only) { - result.collapsed_snapshot_fields = try result.snapshot_fields.collapse(allocator); - } else { - result.collapsed_snapshot_fields = try result.accounts_db.loadWithDefaults( + const collapsed_manifest = if (options.metadata_only) + try combined_manifest.collapse(allocator) + else + try accounts_db.loadWithDefaults( allocator, - &all_snapshot_fields, + combined_manifest, n_threads_snapshot_load, options.validate_snapshot, config.current.accounts_db.accounts_per_file_estimate, config.current.accounts_db.fastload, config.current.accounts_db.save_index, ); - } - errdefer result.collapsed_snapshot_fields.deinit(allocator); - - const bank_fields = &result.collapsed_snapshot_fields.bank_fields; + errdefer collapsed_manifest.deinit(allocator); // this should exist before we start to unpack logger.info().log("reading genesis..."); - result.genesis_config = GenesisConfig.init(allocator, genesis_file_path) catch |err| { + + const genesis_config = GenesisConfig.init(allocator, genesis_file_path) catch |err| { if (err == error.FileNotFound) { logger.err().logf("genesis config not found - expecting {s} to exist", .{genesis_file_path}); } return err; }; - errdefer result.genesis_config.deinit(allocator); + errdefer genesis_config.deinit(allocator); logger.info().log("validating bank..."); - result.bank = Bank.init(&result.accounts_db, bank_fields); - try Bank.validateBankFields(result.bank.bank_fields, &result.genesis_config); + + try Bank.validateBankFields(&collapsed_manifest.bank_fields, &genesis_config); if (options.metadata_only) { - return result; + logger.info().log("accounts-db setup done..."); + return .{ + .allocator = allocator, + .accounts_db = accounts_db, + .combined_manifest = combined_manifest, + .collapsed_manifest = collapsed_manifest, + .genesis_config = genesis_config, + .status_cache = null, + }; } // validate the status cache - result.status_cache = StatusCache.initFromDir(allocator, snapshot_dir) catch |err| { + const status_cache = StatusCache.initFromDir(allocator, snapshot_dir) catch |err| { if (err == error.FileNotFound) { logger.err().logf( "status_cache not found - expecting {s}/snapshots/status_cache to exist", @@ -1452,15 +1464,23 @@ fn loadSnapshot( } return err; }; - errdefer result.status_cache.deinit(allocator); + errdefer status_cache.deinit(allocator); - var slot_history = try result.accounts_db.getSlotHistory(allocator); + const slot_history = try accounts_db.getSlotHistory(allocator); defer slot_history.deinit(allocator); - try result.status_cache.validate(allocator, bank_fields.slot, &slot_history); + + try status_cache.validate(allocator, collapsed_manifest.bank_fields.slot, &slot_history); logger.info().log("accounts-db setup done..."); - return result; + return .{ + .allocator = allocator, + .accounts_db = accounts_db, + .combined_manifest = combined_manifest, + .collapsed_manifest = collapsed_manifest, + .genesis_config = genesis_config, + .status_cache = status_cache, + }; } /// entrypoint to download snapshot @@ -1513,144 +1533,5 @@ fn getTrustedValidators(allocator: Allocator) !?std.ArrayList(Pubkey) { ); } } - return trusted_validators; } - -fn getOrDownloadSnapshots( - allocator: Allocator, - logger_: Logger, - gossip_service: ?*GossipService, - // accounts_db_config: config.AccountsDBConfig, - options: struct { - snapshot_dir: std.fs.Dir, - force_unpack_snapshot: bool, - force_new_snapshot_download: bool, - num_threads_snapshot_unpack: u16, - min_snapshot_download_speed_mbs: usize, - }, -) !struct { AllSnapshotFields, SnapshotFiles } { - const logger = logger_.withScope(LOG_SCOPE); - // arg parsing - const snapshot_dir = options.snapshot_dir; - const force_unpack_snapshot = options.force_unpack_snapshot; - const force_new_snapshot_download = options.force_new_snapshot_download; - - const n_cpus = @as(u32, @truncate(try std.Thread.getCpuCount())); - var n_threads_snapshot_unpack: u32 = options.num_threads_snapshot_unpack; - if (n_threads_snapshot_unpack == 0) { - n_threads_snapshot_unpack = n_cpus * 2; - } - - const maybe_snapshot_files: ?SnapshotFiles = blk: { - if (force_new_snapshot_download) { - break :blk null; - } - - break :blk SnapshotFiles.find(allocator, snapshot_dir) catch |err| switch (err) { - error.NoFullSnapshotFileInfoFound => null, - else => |e| return e, - }; - }; - - const snapshot_files = maybe_snapshot_files orelse blk: { - const trusted_validators = try getTrustedValidators(gpa_allocator); - defer if (trusted_validators) |*tvs| tvs.deinit(); - - const min_mb_per_sec = options.min_snapshot_download_speed_mbs; - try downloadSnapshotsFromGossip( - allocator, - logger.unscoped(), - if (trusted_validators) |trusted| trusted.items else null, - gossip_service orelse return error.SnapshotsNotFoundAndNoGossipService, - snapshot_dir, - @intCast(min_mb_per_sec), - ); - break :blk try SnapshotFiles.find(allocator, snapshot_dir); - }; - - if (snapshot_files.incremental_info == null) { - logger.info().log("no incremental snapshot found"); - } - - // if this exists, we wont look for a .tar.zstd - const accounts_path_exists = !std.meta.isError(snapshot_dir.access("accounts", .{})); - errdefer { - // if something goes wrong, delete the accounts/ directory - // so we unpack the full snapshot the next time. - // - // NOTE: if we didnt do this, we would try to startup with a incomplete - // accounts/ directory the next time we ran the code - see `should_unpack_snapshot`. - snapshot_dir.deleteTree("accounts") catch |err| { - std.debug.print("failed to delete accounts/ dir: {}\n", .{err}); - }; - } - - var should_unpack_snapshot = !accounts_path_exists or force_unpack_snapshot; - if (!should_unpack_snapshot) { - // number of files in accounts/ - var accounts_dir = try snapshot_dir.openDir("accounts", .{}); - defer accounts_dir.close(); - - const dir_size = (try accounts_dir.stat()).size; - if (dir_size <= 100) { - should_unpack_snapshot = true; - logger.info().log("empty accounts/ directory found, will unpack snapshot..."); - } else { - logger.info().log("accounts/ directory found, will not unpack snapshot..."); - } - } - - var timer = try std.time.Timer.start(); - if (should_unpack_snapshot) { - logger.info().log("unpacking snapshots..."); - // if accounts/ doesnt exist then we unpack the found snapshots - // TODO: delete old accounts/ dir if it exists - timer.reset(); - logger.info().logf("unpacking {s}...", .{snapshot_files.full.snapshotArchiveName().constSlice()}); - { - const archive_file = try snapshot_dir.openFile( - snapshot_files.full.snapshotArchiveName().constSlice(), - .{}, - ); - defer archive_file.close(); - try parallelUnpackZstdTarBall( - allocator, - logger.unscoped(), - archive_file, - snapshot_dir, - n_threads_snapshot_unpack, - true, - ); - } - logger.info().logf("unpacked snapshot in {s}", .{std.fmt.fmtDuration(timer.read())}); - - // TODO: can probs do this in parallel with full snapshot - if (snapshot_files.incremental()) |incremental_snapshot| { - timer.reset(); - logger.info().logf("unpacking {s}...", .{incremental_snapshot.snapshotArchiveName().constSlice()}); - - const archive_file = try snapshot_dir.openFile(incremental_snapshot.snapshotArchiveName().constSlice(), .{}); - defer archive_file.close(); - - try parallelUnpackZstdTarBall( - allocator, - logger.unscoped(), - archive_file, - snapshot_dir, - n_threads_snapshot_unpack, - false, - ); - logger.info().logf("unpacked snapshot in {s}", .{std.fmt.fmtDuration(timer.read())}); - } - } else { - logger.info().log("not unpacking snapshot..."); - } - - timer.reset(); - logger.info().log("reading snapshot metadata..."); - const snapshots = try AllSnapshotFields.fromFiles(allocator, logger.unscoped(), snapshot_dir, snapshot_files); - logger.info().logf("read snapshot metdata in {s}", .{std.fmt.fmtDuration(timer.read())}); - - return .{ snapshots, snapshot_files }; -} diff --git a/src/cmd/config.zig b/src/cmd/config.zig index 8f9103a2f..b04f46663 100644 --- a/src/cmd/config.zig +++ b/src/cmd/config.zig @@ -9,6 +9,8 @@ const Cluster = sig.core.Cluster; const SocketAddr = sig.net.SocketAddr; const resolveSocketAddr = sig.net.net.resolveSocketAddr; +const getAccountPerFileEstimateFromCluster = + sig.accounts_db.db.getAccountPerFileEstimateFromCluster; pub const Config = struct { identity: IdentityConfig = .{}, @@ -132,7 +134,9 @@ pub const AccountsDBConfig = struct { /// force download of new snapshot, even if one exists (usually to get a more up-to-date snapshot force_new_snapshot_download: bool = false, /// estimate of the number of accounts per file (used for preallocation) - accounts_per_file_estimate: u64 = 1_500, + accounts_per_file_estimate: u64 = getAccountPerFileEstimateFromCluster(.testnet) catch { + @panic("account_per_file_estimate missing for default cluster"); + }, /// loads accounts-db from pre-existing state which has been saved with the `save_index` option fastload: bool = false, /// saves the accounts index to disk after loading to support fastloading diff --git a/src/core/leader_schedule.zig b/src/core/leader_schedule.zig index f1236acb5..c21815932 100644 --- a/src/core/leader_schedule.zig +++ b/src/core/leader_schedule.zig @@ -14,7 +14,29 @@ const RwMux = sig.sync.RwMux; pub const NUM_CONSECUTIVE_LEADER_SLOTS: u64 = 4; pub const MAX_CACHED_LEADER_SCHEDULES: usize = 10; -pub const SlotLeaderProvider = sig.utils.closure.PointerClosure(Slot, ?Pubkey); +/// interface to express a dependency on slot leaders +pub const SlotLeaders = struct { + state: *anyopaque, + getFn: *const fn (*anyopaque, Slot) ?Pubkey, + + pub fn init( + state: anytype, + getSlotLeader: fn (@TypeOf(state), Slot) ?Pubkey, + ) SlotLeaders { + return .{ + .state = state, + .getFn = struct { + fn genericFn(generic_state: *anyopaque, slot: Slot) ?Pubkey { + return getSlotLeader(@alignCast(@ptrCast(generic_state)), slot); + } + }.genericFn, + }; + } + + pub fn get(self: SlotLeaders, slot: Slot) ?Pubkey { + return self.getFn(self.state, slot); + } +}; /// LeaderScheduleCache is a cache of leader schedules for each epoch. /// Leader schedules are expensive to compute, so this cache is used to avoid @@ -22,10 +44,10 @@ pub const SlotLeaderProvider = sig.utils.closure.PointerClosure(Slot, ?Pubkey); /// LeaderScheduleCache also keeps a copy of the epoch_schedule so that it can /// compute epoch and slot index from a slot. /// NOTE: This struct is not really a 'cache', we should consider renaming it -/// to a SlotLeaderProvider and maybe even moving it outside of the core module. +/// to a SlotLeaders and maybe even moving it outside of the core module. /// This more accurately describes the purpose of this struct as caching is a means /// to an end, not the end itself. It may then follow that we could remove the -/// above pointer closure in favor of passing the SlotLeaderProvider directly. +/// above pointer closure in favor of passing the SlotLeaders directly. pub const LeaderScheduleCache = struct { epoch_schedule: EpochSchedule, leader_schedules: RwMux(std.AutoArrayHashMap(Epoch, LeaderSchedule)), @@ -41,8 +63,8 @@ pub const LeaderScheduleCache = struct { }; } - pub fn slotLeaderProvider(self: *Self) SlotLeaderProvider { - return SlotLeaderProvider.init(self, LeaderScheduleCache.slotLeader); + pub fn slotLeaders(self: *Self) SlotLeaders { + return SlotLeaders.init(self, LeaderScheduleCache.slotLeader); } pub fn put(self: *Self, epoch: Epoch, leader_schedule: LeaderSchedule) !void { diff --git a/src/fuzz.zig b/src/fuzz.zig index fcbadf34d..b0f7b284a 100644 --- a/src/fuzz.zig +++ b/src/fuzz.zig @@ -6,6 +6,7 @@ const accountsdb_fuzz = sig.accounts_db.fuzz; const gossip_fuzz_service = sig.gossip.fuzz_service; const gossip_fuzz_table = sig.gossip.fuzz_table; const accountsdb_snapshot_fuzz = sig.accounts_db.fuzz_snapshot; +const ledger_rocksdb_fuzz = sig.ledger.fuzz_rocksdb; const StandardErrLogger = sig.trace.ChannelPrintLogger; const Level = sig.trace.Level; @@ -22,6 +23,7 @@ pub const FuzzFilter = enum { gossip_service, gossip_table, allocators, + ledger_rocksdb, }; pub fn main() !void { @@ -86,6 +88,7 @@ pub fn main() !void { .snapshot => try accountsdb_snapshot_fuzz.run(&cli_args), .gossip_service => try gossip_fuzz_service.run(seed, &cli_args), .gossip_table => try gossip_fuzz_table.run(seed, &cli_args), + .ledger_rocksdb => try ledger_rocksdb_fuzz.run(seed, &cli_args), .allocators => try sig.utils.allocators.runFuzzer(seed, &cli_args), } } diff --git a/src/gossip/helpers.zig b/src/gossip/helpers.zig new file mode 100644 index 000000000..395be5c97 --- /dev/null +++ b/src/gossip/helpers.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const sig = @import("../sig.zig"); + +const Pubkey = sig.core.Pubkey; +const ContactInfo = sig.gossip.ContactInfo; +const Logger = sig.trace.Logger; +const Cluster = sig.core.Cluster; +const GossipService = sig.gossip.GossipService; +const SocketAddr = sig.net.SocketAddr; +const IpAddr = sig.net.IpAddr; + +const resolveSocketAddr = sig.net.net.resolveSocketAddr; +const getShredAndIPFromEchoServer = sig.net.echo.getShredAndIPFromEchoServer; +const getOrInitIdentity = sig.cmd.helpers.getOrInitIdentity; +const getWallclockMs = sig.time.getWallclockMs; +const getClusterEntrypoints = sig.gossip.service.getClusterEntrypoints; + +/// inits a gossip client with the minimum required configuration +/// relying on the cluster to provide the entrypoints +pub fn initGossipFromCluster( + allocator: std.mem.Allocator, + logger: Logger, + cluster: Cluster, +) !*GossipService { + // gather entrypoints + var entrypoints = std.ArrayList(SocketAddr).init(allocator); + defer entrypoints.deinit(); + + const entrypoints_strs = getClusterEntrypoints(cluster); + for (entrypoints_strs) |entrypoint_str| { + const socket_addr = try resolveSocketAddr(allocator, entrypoint_str); + try entrypoints.append(socket_addr); + } + logger.info().logf("using predefined entrypoints: {any}", .{entrypoints}); + + // create contact info + const echo_data = try getShredAndIPFromEchoServer( + logger.unscoped(), + allocator, + entrypoints.items, + ); + const my_shred_version = echo_data.shred_version orelse 0; + logger.info().logf("my shred version: {d}", .{my_shred_version}); + const my_ip = echo_data.ip orelse IpAddr.newIpv4(127, 0, 0, 1); + logger.info().logf("my ip: {any}", .{my_ip}); + + const default_config = sig.cmd.config.GossipConfig{}; + const my_port = default_config.port; // default port + const my_keypair = try getOrInitIdentity(allocator, logger); + logger.info().logf("gossip_port: {d}", .{my_port}); + + const my_pubkey = Pubkey.fromPublicKey(&my_keypair.public_key); + var contact_info = ContactInfo.init(allocator, my_pubkey, getWallclockMs(), 0); + try contact_info.setSocket(.gossip, SocketAddr.init(my_ip, my_port)); + contact_info.shred_version = my_shred_version; + + // create gossip + return try GossipService.create( + allocator, + allocator, + contact_info, + my_keypair, + entrypoints.items, + logger, + ); +} diff --git a/src/gossip/lib.zig b/src/gossip/lib.zig index 6d48a6f55..adafbd866 100644 --- a/src/gossip/lib.zig +++ b/src/gossip/lib.zig @@ -10,6 +10,7 @@ pub const shards = @import("shards.zig"); pub const table = @import("table.zig"); pub const fuzz_service = @import("fuzz_service.zig"); pub const fuzz_table = @import("fuzz_table.zig"); +pub const helpers = @import("helpers.zig"); pub const ContactInfo = data.ContactInfo; pub const GossipService = service.GossipService; diff --git a/src/gossip/service.zig b/src/gossip/service.zig index 6cf40e340..8f5767057 100644 --- a/src/gossip/service.zig +++ b/src/gossip/service.zig @@ -3297,7 +3297,7 @@ test "init, exit, and deinit" { const logger = test_logger.logger(); - var gossip_service = try GossipService.create( + const gossip_service = try GossipService.create( std.testing.allocator, std.testing.allocator, contact_info, @@ -3311,10 +3311,7 @@ test "init, exit, and deinit" { } const handle = try std.Thread.spawn(.{}, GossipService.run, .{ - gossip_service, .{ - .spy_node = true, - .dump = false, - }, + gossip_service, .{ .spy_node = true, .dump = false }, }); defer { gossip_service.shutdown(); diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig new file mode 100644 index 000000000..7c5ffd402 --- /dev/null +++ b/src/ledger/fuzz.zig @@ -0,0 +1,324 @@ +const std = @import("std"); +const sig = @import("../sig.zig"); + +const ColumnFamily = sig.ledger.database.ColumnFamily; +const AtomicU64 = std.atomic.Value(u64); + +var total_action_count: AtomicU64 = AtomicU64.init(0); + +const allocator = std.heap.c_allocator; + +const Data = struct { + value: []const u8, +}; + +const cf1 = ColumnFamily{ + .name = "data", + .Key = u64, + .Value = Data, +}; +const RocksDb = sig.ledger.database.RocksDB(&.{cf1}); + +pub fn run(seed: u64, args: *std.process.ArgIterator) !void { + const maybe_max_actions_string = args.next(); + const maybe_max_actions = blk: { + if (maybe_max_actions_string) |max_actions_str| { + break :blk try std.fmt.parseInt(usize, max_actions_str, 10); + } else { + break :blk null; + } + }; + defer { + _ = total_action_count.fetchAdd(1, .monotonic); + } + + // NOTE: change to trace for full logs + var std_logger = sig.trace.DirectPrintLogger.init( + allocator, + .debug, + ); + const logger = std_logger.logger(); + + var prng = std.rand.DefaultPrng.init(seed); + const random = prng.random(); + + const rocksdb_path = + try std.fmt.allocPrint(allocator, "{s}/ledger/rocksdb", .{sig.FUZZ_DATA_DIR}); + + // ensure we start with a clean slate. + if (std.fs.cwd().access(rocksdb_path, .{})) |_| { + try std.fs.cwd().deleteTree(rocksdb_path); + } else |_| {} + try std.fs.cwd().makePath(rocksdb_path); + + var db: RocksDb = try RocksDb.open( + allocator, + logger, + rocksdb_path, + ); + + defer db.deinit(); + + { + var db_put_thread = try std.Thread.spawn( + .{}, + dbPut, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_put_thread.join(); + + var db_delete_thread = try std.Thread.spawn( + .{}, + dbDelete, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_delete_thread.join(); + + var db_delete_files_in_range = try std.Thread.spawn( + .{}, + dbDeleteFilesInRange, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_delete_files_in_range.join(); + + var db_get_bytes_thread = try std.Thread.spawn( + .{}, + dbGetBytes, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_get_bytes_thread.join(); + + var db_get_thread = try std.Thread.spawn( + .{}, + dbGet, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_get_thread.join(); + + var db_count_thread = try std.Thread.spawn( + .{}, + dbCount, + .{ &db, &total_action_count, maybe_max_actions }, + ); + defer db_count_thread.join(); + + var db_contains_thread = try std.Thread.spawn( + .{}, + dbContains, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer db_contains_thread.join(); + + // Batch API + var batch_delete_range_thread = try std.Thread.spawn( + .{}, + batchDeleteRange, + .{ &db, &random, &total_action_count, maybe_max_actions }, + ); + defer batch_delete_range_thread.join(); + } +} + +fn performDbAction( + action_name: []const u8, + comptime func: anytype, + args: anytype, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + var last_print_msg_count: u64 = 0; + + while (true) { + if (max_actions) |max| { + if (count.load(.monotonic) >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + _ = try @call(.auto, func, args); + const current_count = count.load(.monotonic); + if ((current_count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ current_count, action_name }); + last_print_msg_count = current_count; + } + + _ = count.fetchAdd(1, .monotonic); + } +} + +fn dbPut( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const key = random.int(u32); + var buffer: [61]u8 = undefined; + // Fill the buffer with random bytes + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + const value: []const u8 = buffer[0..]; + try performDbAction( + "RocksDb.put", + RocksDb.put, + .{ db, cf1, (key + 1), Data{ .value = value } }, + count, + max_actions, + ); +} + +fn dbDelete( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const key = random.int(u32); + try performDbAction( + "RocksDb.delete", + RocksDb.delete, + .{ db, cf1, key }, + count, + max_actions, + ); +} + +fn dbDeleteFilesInRange( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const start = random.int(u32); + const end = blk: { + const end_ = random.int(u32); + if (end_ < start) + break :blk (end_ +| start) + else + break :blk end_; + }; + + try performDbAction( + "RocksDb.deleteFilesInRange", + RocksDb.deleteFilesInRange, + .{ db, cf1, start, end }, + count, + max_actions, + ); +} + +fn dbGetBytes( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const key = random.int(u32); + try performDbAction( + "RocksDb.getBytes", + RocksDb.getBytes, + .{ db, cf1, key }, + count, + max_actions, + ); +} + +fn dbGet( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const key = random.int(u32); + try performDbAction( + "RocksDb.get", + RocksDb.get, + .{ db, allocator, cf1, key }, + count, + max_actions, + ); +} + +fn dbCount( + db: *RocksDb, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + try performDbAction( + "RocksDb.count", + RocksDb.count, + .{ db, cf1 }, + count, + max_actions, + ); +} + +fn dbContains( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + const key = random.int(u32); + try performDbAction( + "RocksDb.contains", + RocksDb.contains, + .{ db, cf1, key }, + count, + max_actions, + ); +} + +// Batch API +fn batchDeleteRange( + db: *RocksDb, + random: *const std.rand.Random, + count: *std.atomic.Value(u64), + max_actions: ?usize, +) !void { + var last_print_msg_count: u64 = 0; + while (true) { + if (max_actions) |max| { + if (count.load(.monotonic) >= max) { + std.debug.print("Batch actions reached max actions: {}\n", .{max}); + break; + } + } + const start = random.int(u32); + const end = blk: { + const end_ = random.int(u32); + if (end_ < start) + break :blk (end_ +| start) + else + break :blk end_; + }; + + const key = random.int(u32); + var buffer: [61]u8 = undefined; + + // Fill the buffer with random bytes + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + + const value: []const u8 = buffer[0..]; + + var batch = try db.initWriteBatch(); + defer batch.deinit(); + + try batch.put(cf1, key, Data{ .value = value }); + try batch.deleteRange(cf1, start, end); + try batch.delete(cf1, key); + try db.commit(&batch); + + const current_count = count.load(.monotonic); + if ((current_count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} Batch actions\n", .{current_count}); + last_print_msg_count = current_count; + } + + _ = count.fetchAdd(1, .monotonic); + } +} diff --git a/src/ledger/lib.zig b/src/ledger/lib.zig index 9e8c74730..acae6e67c 100644 --- a/src/ledger/lib.zig +++ b/src/ledger/lib.zig @@ -11,6 +11,7 @@ pub const shred = @import("shred.zig"); pub const shredder = @import("shredder.zig"); pub const transaction_status = @import("transaction_status.zig"); pub const tests = @import("tests.zig"); +pub const fuzz_rocksdb = @import("fuzz.zig"); pub const BlockstoreDB = blockstore.BlockstoreDB; pub const ShredInserter = shred_inserter.ShredInserter; diff --git a/src/ledger/shred_inserter/shred_inserter.zig b/src/ledger/shred_inserter/shred_inserter.zig index b3536783d..f4cc130b6 100644 --- a/src/ledger/shred_inserter/shred_inserter.zig +++ b/src/ledger/shred_inserter/shred_inserter.zig @@ -23,7 +23,7 @@ const CodeShred = ledger.shred.CodeShred; const DataShred = ledger.shred.DataShred; const ReedSolomonCache = lib.recovery.ReedSolomonCache; const ShredId = ledger.shred.ShredId; -const SlotLeaderProvider = sig.core.leader_schedule.SlotLeaderProvider; +const SlotLeaders = sig.core.leader_schedule.SlotLeaders; const SortedSet = sig.utils.collections.SortedSet; const SortedMap = sig.utils.collections.SortedMap; const Timer = sig.time.Timer; @@ -145,7 +145,7 @@ pub const ShredInserter = struct { self: *Self, shreds: []const Shred, is_repaired: []const bool, - leader_schedule: ?SlotLeaderProvider, + maybe_slot_leaders: ?SlotLeaders, is_trusted: bool, retransmit_sender: ?PointerClosure([]const []const u8, void), ) !InsertShredsResult { @@ -195,7 +195,7 @@ pub const ShredInserter = struct { merkle_root_validator, write_batch, is_trusted, - leader_schedule, + maybe_slot_leaders, shred_source, )) |completed_data_sets| { if (is_repair) { @@ -239,7 +239,7 @@ pub const ShredInserter = struct { var shred_recovery_timer = try Timer.start(); var valid_recovered_shreds = ArrayList([]const u8).init(allocator); defer valid_recovered_shreds.deinit(); - if (leader_schedule) |slot_leader_provider| { + if (maybe_slot_leaders) |slot_leaders| { var reed_solomon_cache = try ReedSolomonCache.init(allocator); defer reed_solomon_cache.deinit(); const recovered_shreds = try self.tryShredRecovery( @@ -259,7 +259,7 @@ pub const ShredInserter = struct { if (shred == .data) { self.metrics.num_recovered.inc(); } - const leader = slot_leader_provider.call(shred.commonHeader().slot); + const leader = slot_leaders.get(shred.commonHeader().slot); if (leader == null) { continue; } @@ -280,7 +280,7 @@ pub const ShredInserter = struct { merkle_root_validator, write_batch, is_trusted, - leader_schedule, + maybe_slot_leaders, .recovered, )) |completed_data_sets| { defer completed_data_sets.deinit(); @@ -590,7 +590,7 @@ pub const ShredInserter = struct { merkle_root_validator: MerkleRootValidator, write_batch: *WriteBatch, is_trusted: bool, - leader_schedule: ?SlotLeaderProvider, + leader_schedule: ?SlotLeaders, shred_source: ShredSource, ) !ArrayList(CompletedDataSetInfo) { const slot = shred.common.slot; @@ -708,7 +708,7 @@ pub const ShredInserter = struct { slot_meta: *const SlotMeta, shred_store: ShredWorkingStore, max_root: Slot, - leader_schedule: ?SlotLeaderProvider, + leader_schedule: ?SlotLeaders, shred_source: ShredSource, duplicate_shreds: *ArrayList(PossibleDuplicateShred), ) !bool { @@ -975,8 +975,8 @@ fn verifyShredSlots(slot: Slot, parent: Slot, root: Slot) bool { return root <= parent and parent < slot; } -fn slotLeader(provider: ?SlotLeaderProvider, slot: Slot) ?Pubkey { - return if (provider) |p| if (p.call(slot)) |l| l else null else null; +fn slotLeader(provider: ?SlotLeaders, slot: Slot) ?Pubkey { + return if (provider) |p| if (p.get(slot)) |l| l else null else null; } /// update_slot_meta @@ -1486,7 +1486,7 @@ test "recovery" { const data_shreds = shreds[0..34]; const code_shreds = shreds[34..68]; - var leader_schedule = OneSlotLeaderProvider{ + var leader_schedule = OneSlotLeaders{ .leader = try Pubkey.fromString("2iWGQbhdWWAA15KTBJuqvAxCdKmEvY26BoFRBU4419Sn"), }; @@ -1512,15 +1512,15 @@ test "recovery" { // TODO: verify index integrity } -const OneSlotLeaderProvider = struct { +const OneSlotLeaders = struct { leader: Pubkey, - fn getLeader(self: *OneSlotLeaderProvider, _: Slot) ?Pubkey { + fn getLeader(self: *OneSlotLeaders, _: Slot) ?Pubkey { return self.leader; } - fn provider(self: *OneSlotLeaderProvider) SlotLeaderProvider { - return SlotLeaderProvider.init(self, OneSlotLeaderProvider.getLeader); + fn provider(self: *OneSlotLeaders) SlotLeaders { + return SlotLeaders.init(self, OneSlotLeaders.getLeader); } }; diff --git a/src/rpc/server.zig b/src/rpc/server.zig index c2d17c0e4..c320e3900 100644 --- a/src/rpc/server.zig +++ b/src/rpc/server.zig @@ -436,15 +436,6 @@ test Server { try unpack(allocator, logger, inc_snap_file, snap_dir, 1, false); } - const AllSnapshotFields = sig.accounts_db.snapshots.AllSnapshotFields; - var all_snap_fields = try AllSnapshotFields.fromFiles( - allocator, - logger, - snap_dir, - snap_files, - ); - defer all_snap_fields.deinit(allocator); - var accountsdb = try sig.accounts_db.AccountsDB.init(.{ .allocator = allocator, .logger = logger, @@ -456,7 +447,27 @@ test Server { .lru_size = null, }); defer accountsdb.deinit(); - _ = try accountsdb.loadWithDefaults(allocator, &all_snap_fields, 1, true, 300, false, false); + + { + const FullAndIncrementalManifest = sig.accounts_db.snapshots.FullAndIncrementalManifest; + const all_snap_fields = try FullAndIncrementalManifest.fromFiles( + allocator, + logger, + snap_dir, + snap_files, + ); + defer all_snap_fields.deinit(allocator); + + (try accountsdb.loadWithDefaults( + allocator, + all_snap_fields, + 1, + true, + 300, + false, + false, + )).deinit(allocator); + } var thread_pool = sig.sync.ThreadPool.init(.{ .max_threads = 1 }); defer { diff --git a/src/shred_network/service.zig b/src/shred_network/service.zig index ab15f671a..ced4594b6 100644 --- a/src/shred_network/service.zig +++ b/src/shred_network/service.zig @@ -19,7 +19,7 @@ const RwMux = sig.sync.RwMux; const Registry = sig.prometheus.Registry; const ServiceManager = sig.utils.service_manager.ServiceManager; const Slot = sig.core.Slot; -const SlotLeaderProvider = sig.core.leader_schedule.SlotLeaderProvider; +const SlotLeaders = sig.core.leader_schedule.SlotLeaders; const LeaderScheduleCache = sig.core.leader_schedule.LeaderScheduleCache; const BasicShredTracker = shred_network.shred_tracker.BasicShredTracker; @@ -52,7 +52,7 @@ pub const ShredCollectorDependencies = struct { /// Shared state that is read from gossip my_shred_version: *const Atomic(u16), my_contact_info: ThreadSafeContactInfo, - leader_schedule: SlotLeaderProvider, + leader_schedule: SlotLeaders, shred_inserter: sig.ledger.ShredInserter, n_retransmit_threads: ?usize, overwrite_turbine_stake_for_testing: bool, diff --git a/src/shred_network/shred_processor.zig b/src/shred_network/shred_processor.zig index 0e1681df9..9b018c0e4 100644 --- a/src/shred_network/shred_processor.zig +++ b/src/shred_network/shred_processor.zig @@ -33,7 +33,7 @@ pub fn runShredProcessor( verified_shred_receiver: *Channel(Packet), tracker: *BasicShredTracker, shred_inserter_: ShredInserter, - leader_schedule: sig.core.leader_schedule.SlotLeaderProvider, + leader_schedule: sig.core.leader_schedule.SlotLeaders, ) !void { const logger = logger_.withScope(LOG_SCOPE); var shred_inserter = shred_inserter_; diff --git a/src/shred_network/shred_verifier.zig b/src/shred_network/shred_verifier.zig index 9aee2d3b1..1c7332df1 100644 --- a/src/shred_network/shred_verifier.zig +++ b/src/shred_network/shred_verifier.zig @@ -10,7 +10,7 @@ const Counter = sig.prometheus.Counter; const Histogram = sig.prometheus.Histogram; const Packet = sig.net.Packet; const Registry = sig.prometheus.Registry; -const SlotLeaderProvider = sig.core.leader_schedule.SlotLeaderProvider; +const SlotLeaders = sig.core.leader_schedule.SlotLeaders; const VariantCounter = sig.prometheus.VariantCounter; const VerifiedMerkleRoots = sig.common.lru.LruCache(.non_locking, sig.core.Hash, void); @@ -25,7 +25,7 @@ pub fn runShredVerifier( verified_shred_sender: *Channel(Packet), /// me --> retransmit service maybe_retransmit_shred_sender: ?*Channel(Packet), - leader_schedule: SlotLeaderProvider, + leader_schedule: SlotLeaders, ) !void { const metrics = try registry.initStruct(Metrics); var verified_merkle_roots = try VerifiedMerkleRoots.init(std.heap.c_allocator, 1024); @@ -53,7 +53,7 @@ pub fn runShredVerifier( /// Analogous to [verify_shred_cpu](https://github.com/anza-xyz/agave/blob/83e7d84bcc4cf438905d07279bc07e012a49afd9/ledger/src/sigverify_shreds.rs#L35) fn verifyShred( packet: *const Packet, - leader_schedule: SlotLeaderProvider, + leader_schedule: SlotLeaders, verified_merkle_roots: *VerifiedMerkleRoots, metrics: Metrics, ) ShredVerificationFailure!void { @@ -66,7 +66,7 @@ fn verifyShred( return; } metrics.cache_miss_count.inc(); - const leader = leader_schedule.call(slot) orelse return error.leader_unknown; + const leader = leader_schedule.get(slot) orelse return error.leader_unknown; const valid = signature.verify(leader, &signed_data.data) catch return error.failed_verification; if (!valid) return error.failed_verification; diff --git a/src/sig.zig b/src/sig.zig index 771c2e47f..3d8f41d3c 100644 --- a/src/sig.zig +++ b/src/sig.zig @@ -21,6 +21,8 @@ pub const transaction_sender = @import("transaction_sender/lib.zig"); pub const cmd = @import("cmd/lib.zig"); pub const VALIDATOR_DIR = "validator/"; +/// subdirectory of {VALIDATOR_DIR} which contains the accounts database +pub const ACCOUNTS_DB_SUBDIR = "accounts_db/"; /// persistent data used as test inputs pub const TEST_DATA_DIR = "data/test-data/"; /// ephemeral state produced by tests diff --git a/src/sync/reference_counter.zig b/src/sync/reference_counter.zig index 645463ddd..59196c63f 100644 --- a/src/sync/reference_counter.zig +++ b/src/sync/reference_counter.zig @@ -72,6 +72,26 @@ pub const ReferenceCounter = extern struct { } return false; } + + /// Checks that our resource still has references. + /// + /// Returns: + /// - true: there is at least 1 remaining reference + /// - false: there are no more references + pub fn isAlive(self: *Self) bool { + const current: State = @bitCast(self.state.load(.seq_cst)); + return current.refs >= 1; + } + + /// Resets a reference count representing a dead resource (rc=0) to one + /// representing an alive resource (rc=1). + pub fn reset(self: *Self) void { + const prior: State = @bitCast(self.state.load(.acquire)); + if (prior.refs != 0) { + unreachable; // tried to reset alive reference counter + } + self.state.store(@bitCast(State{ .refs = 1 }), .release); + } }; /// A reference counted slice that is only freed when the last From c86db1fa4ba852245c366d67afd2899ea2526d7c Mon Sep 17 00:00:00 2001 From: oluwadadepo aderemi <272535+dadepo@users.noreply.github.com> Date: Wed, 1 Jan 2025 20:58:07 +0400 Subject: [PATCH 02/25] Fix build --- src/fuzz.zig | 6 +++--- src/ledger/fuzz.zig | 40 +++++++++++++++++++++++----------------- src/ledger/lib.zig | 2 +- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/fuzz.zig b/src/fuzz.zig index b0f7b284a..7339a76ab 100644 --- a/src/fuzz.zig +++ b/src/fuzz.zig @@ -6,7 +6,7 @@ const accountsdb_fuzz = sig.accounts_db.fuzz; const gossip_fuzz_service = sig.gossip.fuzz_service; const gossip_fuzz_table = sig.gossip.fuzz_table; const accountsdb_snapshot_fuzz = sig.accounts_db.fuzz_snapshot; -const ledger_rocksdb_fuzz = sig.ledger.fuzz_rocksdb; +const ledger_fuzz = sig.ledger.fuzz_ledger; const StandardErrLogger = sig.trace.ChannelPrintLogger; const Level = sig.trace.Level; @@ -23,7 +23,7 @@ pub const FuzzFilter = enum { gossip_service, gossip_table, allocators, - ledger_rocksdb, + ledger, }; pub fn main() !void { @@ -88,7 +88,7 @@ pub fn main() !void { .snapshot => try accountsdb_snapshot_fuzz.run(&cli_args), .gossip_service => try gossip_fuzz_service.run(seed, &cli_args), .gossip_table => try gossip_fuzz_table.run(seed, &cli_args), - .ledger_rocksdb => try ledger_rocksdb_fuzz.run(seed, &cli_args), + .ledger => try ledger_fuzz.run(seed, &cli_args), .allocators => try sig.utils.allocators.runFuzzer(seed, &cli_args), } } diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 7c5ffd402..49fba6f1b 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -17,7 +17,13 @@ const cf1 = ColumnFamily{ .Key = u64, .Value = Data, }; -const RocksDb = sig.ledger.database.RocksDB(&.{cf1}); +pub const BlockstoreDB = switch (build_options.blockstore_db) { + .rocksdb => ledger.database.RocksDB(&.{cf1}), + .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), +}; + +const build_options = @import("build-options"); +const ledger = @import("lib.zig"); pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const maybe_max_actions_string = args.next(); @@ -51,7 +57,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } else |_| {} try std.fs.cwd().makePath(rocksdb_path); - var db: RocksDb = try RocksDb.open( + var db: BlockstoreDB = try BlockstoreDB.open( allocator, logger, rocksdb_path, @@ -148,7 +154,7 @@ fn performDbAction( } fn dbPut( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -162,7 +168,7 @@ fn dbPut( const value: []const u8 = buffer[0..]; try performDbAction( "RocksDb.put", - RocksDb.put, + BlockstoreDB.put, .{ db, cf1, (key + 1), Data{ .value = value } }, count, max_actions, @@ -170,7 +176,7 @@ fn dbPut( } fn dbDelete( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -178,7 +184,7 @@ fn dbDelete( const key = random.int(u32); try performDbAction( "RocksDb.delete", - RocksDb.delete, + BlockstoreDB.delete, .{ db, cf1, key }, count, max_actions, @@ -186,7 +192,7 @@ fn dbDelete( } fn dbDeleteFilesInRange( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -202,7 +208,7 @@ fn dbDeleteFilesInRange( try performDbAction( "RocksDb.deleteFilesInRange", - RocksDb.deleteFilesInRange, + BlockstoreDB.deleteFilesInRange, .{ db, cf1, start, end }, count, max_actions, @@ -210,7 +216,7 @@ fn dbDeleteFilesInRange( } fn dbGetBytes( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -218,7 +224,7 @@ fn dbGetBytes( const key = random.int(u32); try performDbAction( "RocksDb.getBytes", - RocksDb.getBytes, + BlockstoreDB.getBytes, .{ db, cf1, key }, count, max_actions, @@ -226,7 +232,7 @@ fn dbGetBytes( } fn dbGet( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -234,7 +240,7 @@ fn dbGet( const key = random.int(u32); try performDbAction( "RocksDb.get", - RocksDb.get, + BlockstoreDB.get, .{ db, allocator, cf1, key }, count, max_actions, @@ -242,13 +248,13 @@ fn dbGet( } fn dbCount( - db: *RocksDb, + db: *BlockstoreDB, count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { try performDbAction( "RocksDb.count", - RocksDb.count, + BlockstoreDB.count, .{ db, cf1 }, count, max_actions, @@ -256,7 +262,7 @@ fn dbCount( } fn dbContains( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, @@ -264,7 +270,7 @@ fn dbContains( const key = random.int(u32); try performDbAction( "RocksDb.contains", - RocksDb.contains, + BlockstoreDB.contains, .{ db, cf1, key }, count, max_actions, @@ -273,7 +279,7 @@ fn dbContains( // Batch API fn batchDeleteRange( - db: *RocksDb, + db: *BlockstoreDB, random: *const std.rand.Random, count: *std.atomic.Value(u64), max_actions: ?usize, diff --git a/src/ledger/lib.zig b/src/ledger/lib.zig index acae6e67c..d4617f653 100644 --- a/src/ledger/lib.zig +++ b/src/ledger/lib.zig @@ -11,7 +11,7 @@ pub const shred = @import("shred.zig"); pub const shredder = @import("shredder.zig"); pub const transaction_status = @import("transaction_status.zig"); pub const tests = @import("tests.zig"); -pub const fuzz_rocksdb = @import("fuzz.zig"); +pub const fuzz_ledger = @import("fuzz.zig"); pub const BlockstoreDB = blockstore.BlockstoreDB; pub const ShredInserter = shred_inserter.ShredInserter; From c7efe51d70e31b6f8575e2298fa7793946060cb4 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:23:45 +0000 Subject: [PATCH 03/25] Have all imports at top of file --- src/ledger/fuzz.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 49fba6f1b..13eac2f33 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -1,5 +1,7 @@ const std = @import("std"); const sig = @import("../sig.zig"); +const build_options = @import("build-options"); +const ledger = @import("lib.zig"); const ColumnFamily = sig.ledger.database.ColumnFamily; const AtomicU64 = std.atomic.Value(u64); @@ -22,8 +24,6 @@ pub const BlockstoreDB = switch (build_options.blockstore_db) { .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; -const build_options = @import("build-options"); -const ledger = @import("lib.zig"); pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const maybe_max_actions_string = args.next(); From efd4025635518bc6fb9ee95e71f1868bfdfc6a5a Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:03:01 +0000 Subject: [PATCH 04/25] Zig fmt --- src/ledger/fuzz.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 13eac2f33..885e52037 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -24,7 +24,6 @@ pub const BlockstoreDB = switch (build_options.blockstore_db) { .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; - pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const maybe_max_actions_string = args.next(); const maybe_max_actions = blk: { From 52bbbb212263ef81abab187ef584661a398f4f94 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:03:37 +0000 Subject: [PATCH 05/25] Remove explicit type annotation --- src/ledger/fuzz.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 885e52037..2d6cd2018 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -56,7 +56,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } else |_| {} try std.fs.cwd().makePath(rocksdb_path); - var db: BlockstoreDB = try BlockstoreDB.open( + var db = try BlockstoreDB.open( allocator, logger, rocksdb_path, From b419b4becebf3ad18e872e4005915161f9f2f91a Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:24:13 +0000 Subject: [PATCH 06/25] Remove concurrency --- src/ledger/fuzz.zig | 121 ++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 90 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 2d6cd2018..0741960a7 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -4,9 +4,6 @@ const build_options = @import("build-options"); const ledger = @import("lib.zig"); const ColumnFamily = sig.ledger.database.ColumnFamily; -const AtomicU64 = std.atomic.Value(u64); - -var total_action_count: AtomicU64 = AtomicU64.init(0); const allocator = std.heap.c_allocator; @@ -33,9 +30,6 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { break :blk null; } }; - defer { - _ = total_action_count.fetchAdd(1, .monotonic); - } // NOTE: change to trace for full logs var std_logger = sig.trace.DirectPrintLogger.init( @@ -64,63 +58,23 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { defer db.deinit(); - { - var db_put_thread = try std.Thread.spawn( - .{}, - dbPut, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_put_thread.join(); - - var db_delete_thread = try std.Thread.spawn( - .{}, - dbDelete, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_delete_thread.join(); - - var db_delete_files_in_range = try std.Thread.spawn( - .{}, - dbDeleteFilesInRange, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_delete_files_in_range.join(); - - var db_get_bytes_thread = try std.Thread.spawn( - .{}, - dbGetBytes, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_get_bytes_thread.join(); - - var db_get_thread = try std.Thread.spawn( - .{}, - dbGet, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_get_thread.join(); - - var db_count_thread = try std.Thread.spawn( - .{}, - dbCount, - .{ &db, &total_action_count, maybe_max_actions }, - ); - defer db_count_thread.join(); - - var db_contains_thread = try std.Thread.spawn( - .{}, - dbContains, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer db_contains_thread.join(); - + const functions = .{ + dbPut, + dbDelete, + dbDeleteFilesInRange, + dbGetBytes, + dbGet, + dbCount, + dbContains, // Batch API - var batch_delete_range_thread = try std.Thread.spawn( - .{}, - batchDeleteRange, - .{ &db, &random, &total_action_count, maybe_max_actions }, - ); - defer batch_delete_range_thread.join(); + batchDeleteRange, + }; + + const maybe_max_action = if (maybe_max_actions) |max| max / functions.len else null; + + inline for (functions) |function| { + const fn_args = .{ &db, &random, maybe_max_action }; + _ = try @call(.auto, function, fn_args); } } @@ -128,34 +82,32 @@ fn performDbAction( action_name: []const u8, comptime func: anytype, args: anytype, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { + var count: u64 = 0; var last_print_msg_count: u64 = 0; while (true) { if (max_actions) |max| { - if (count.load(.monotonic) >= max) { + if (count >= max) { std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); break; } } _ = try @call(.auto, func, args); - const current_count = count.load(.monotonic); - if ((current_count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ current_count, action_name }); - last_print_msg_count = current_count; + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; } - _ = count.fetchAdd(1, .monotonic); + count += 1; } } fn dbPut( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const key = random.int(u32); @@ -169,7 +121,6 @@ fn dbPut( "RocksDb.put", BlockstoreDB.put, .{ db, cf1, (key + 1), Data{ .value = value } }, - count, max_actions, ); } @@ -177,7 +128,6 @@ fn dbPut( fn dbDelete( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const key = random.int(u32); @@ -185,7 +135,6 @@ fn dbDelete( "RocksDb.delete", BlockstoreDB.delete, .{ db, cf1, key }, - count, max_actions, ); } @@ -193,7 +142,6 @@ fn dbDelete( fn dbDeleteFilesInRange( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const start = random.int(u32); @@ -209,7 +157,6 @@ fn dbDeleteFilesInRange( "RocksDb.deleteFilesInRange", BlockstoreDB.deleteFilesInRange, .{ db, cf1, start, end }, - count, max_actions, ); } @@ -217,7 +164,6 @@ fn dbDeleteFilesInRange( fn dbGetBytes( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const key = random.int(u32); @@ -225,7 +171,6 @@ fn dbGetBytes( "RocksDb.getBytes", BlockstoreDB.getBytes, .{ db, cf1, key }, - count, max_actions, ); } @@ -233,7 +178,6 @@ fn dbGetBytes( fn dbGet( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const key = random.int(u32); @@ -241,21 +185,21 @@ fn dbGet( "RocksDb.get", BlockstoreDB.get, .{ db, allocator, cf1, key }, - count, max_actions, ); } fn dbCount( db: *BlockstoreDB, - count: *std.atomic.Value(u64), + // Unused. Listed to allow uniform call + // via @call with the rest of the functions. + _: *const std.rand.Random, max_actions: ?usize, ) !void { try performDbAction( "RocksDb.count", BlockstoreDB.count, .{ db, cf1 }, - count, max_actions, ); } @@ -263,7 +207,6 @@ fn dbCount( fn dbContains( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { const key = random.int(u32); @@ -271,7 +214,6 @@ fn dbContains( "RocksDb.contains", BlockstoreDB.contains, .{ db, cf1, key }, - count, max_actions, ); } @@ -280,13 +222,13 @@ fn dbContains( fn batchDeleteRange( db: *BlockstoreDB, random: *const std.rand.Random, - count: *std.atomic.Value(u64), max_actions: ?usize, ) !void { + var count: u64 = 0; var last_print_msg_count: u64 = 0; while (true) { if (max_actions) |max| { - if (count.load(.monotonic) >= max) { + if (count >= max) { std.debug.print("Batch actions reached max actions: {}\n", .{max}); break; } @@ -318,12 +260,11 @@ fn batchDeleteRange( try batch.delete(cf1, key); try db.commit(&batch); - const current_count = count.load(.monotonic); - if ((current_count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} Batch actions\n", .{current_count}); - last_print_msg_count = current_count; + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} Batch actions\n", .{count}); + last_print_msg_count = count; } - _ = count.fetchAdd(1, .monotonic); + count += 1; } } From e73776cd02424430f4654a4db34a452da4182fa8 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:38:38 +0000 Subject: [PATCH 07/25] No need to divide up the provided max action --- src/ledger/fuzz.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 0741960a7..6d8b29691 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -70,10 +70,8 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { batchDeleteRange, }; - const maybe_max_action = if (maybe_max_actions) |max| max / functions.len else null; - inline for (functions) |function| { - const fn_args = .{ &db, &random, maybe_max_action }; + const fn_args = .{ &db, &random, maybe_max_actions }; _ = try @call(.auto, function, fn_args); } } From 9e06b51485f7c019d6d2404e4ade42e270ac008a Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:40:31 +0000 Subject: [PATCH 08/25] Assert result of ledger calls --- src/ledger/fuzz.zig | 386 +++++++++++++++++++++++++++++++------------- 1 file changed, 278 insertions(+), 108 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 6d8b29691..b2bfb5359 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -16,6 +16,10 @@ const cf1 = ColumnFamily{ .Key = u64, .Value = Data, }; + +var dataMap = std.AutoHashMap(u32, Data).init(allocator); +var dataKeys = std.ArrayList(u32).init(allocator); + pub const BlockstoreDB = switch (build_options.blockstore_db) { .rocksdb => ledger.database.RocksDB(&.{cf1}), .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), @@ -60,14 +64,14 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const functions = .{ dbPut, - dbDelete, - dbDeleteFilesInRange, - dbGetBytes, dbGet, - dbCount, + dbGetBytes, + // dbCount, dbContains, + dbDelete, + // dbDeleteFilesInRange, // Batch API - batchDeleteRange, + batchOps, }; inline for (functions) |function| { @@ -76,14 +80,14 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } } -fn performDbAction( - action_name: []const u8, - comptime func: anytype, - args: anytype, +fn dbPut( + db: *BlockstoreDB, + random: *const std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; var last_print_msg_count: u64 = 0; + const action_name = "put"; while (true) { if (max_actions) |max| { @@ -93,7 +97,22 @@ fn performDbAction( } } - _ = try @call(.auto, func, args); + const key = random.int(u32); + var buffer: [61]u8 = undefined; + + // Fill the buffer with random bytes + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try BlockstoreDB.put(db, cf1, key, data); + try dataMap.put(key, data); + std.debug.print("insert key {d}, max {d}\n", .{ key, std.math.maxInt(u32) }); + try dataKeys.append(key); + if ((count - last_print_msg_count) >= 1_000) { std.debug.print("{d} {s} actions\n", .{ count, action_name }); last_print_msg_count = count; @@ -103,125 +122,217 @@ fn performDbAction( } } -fn dbPut( +fn dbGet( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { - const key = random.int(u32); - var buffer: [61]u8 = undefined; - // Fill the buffer with random bytes - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "get"; + + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const random_index = random.uintLessThan(usize, dataKeys.items.len); + const key = dataKeys.items[random_index]; + const expected = dataMap.get(key) orelse return error.KeyNotFoundError; + + const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse return error.KeyNotFoundError; + + try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; } - const value: []const u8 = buffer[0..]; - try performDbAction( - "RocksDb.put", - BlockstoreDB.put, - .{ db, cf1, (key + 1), Data{ .value = value } }, - max_actions, - ); } -fn dbDelete( +fn dbGetBytes( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { - const key = random.int(u32); - try performDbAction( - "RocksDb.delete", - BlockstoreDB.delete, - .{ db, cf1, key }, - max_actions, - ); + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "getBytes"; + + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const random_index = random.uintLessThan(usize, dataKeys.items.len); + const key = dataKeys.items[random_index]; + const expected = dataMap.get(key) orelse return error.KeyNotFoundError; + + const actualBytes = try BlockstoreDB.getBytes(db, cf1, key) orelse return error.KeyNotFoundError; + const actual = try ledger.database.value_serializer.deserialize(cf1.Value, allocator, actualBytes.data); + + try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; + } } -fn dbDeleteFilesInRange( +fn dbCount( db: *BlockstoreDB, - random: *const std.rand.Random, + // Unused. Listed to allow uniform call + // via @call with the rest of the functions. + _: *const std.rand.Random, max_actions: ?usize, ) !void { - const start = random.int(u32); - const end = blk: { - const end_ = random.int(u32); - if (end_ < start) - break :blk (end_ +| start) - else - break :blk end_; - }; + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "count"; - try performDbAction( - "RocksDb.deleteFilesInRange", - BlockstoreDB.deleteFilesInRange, - .{ db, cf1, start, end }, - max_actions, - ); + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const expected = dataKeys.items.len; + const actual = try BlockstoreDB.count(db, cf1); + + try std.testing.expectEqual(expected, actual); + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; + } } -fn dbGetBytes( +fn dbContains( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { - const key = random.int(u32); - try performDbAction( - "RocksDb.getBytes", - BlockstoreDB.getBytes, - .{ db, cf1, key }, - max_actions, - ); + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "contains"; + + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const random_index = random.uintLessThan(usize, dataKeys.items.len); + const key = dataKeys.items[random_index]; + + const actual = try BlockstoreDB.contains(db, cf1, key); + + try std.testing.expect(actual); + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; + } } -fn dbGet( +fn dbDeleteFilesInRange( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { - const key = random.int(u32); - try performDbAction( - "RocksDb.get", - BlockstoreDB.get, - .{ db, allocator, cf1, key }, - max_actions, - ); -} + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "deleteFilesInRange"; -fn dbCount( - db: *BlockstoreDB, - // Unused. Listed to allow uniform call - // via @call with the rest of the functions. - _: *const std.rand.Random, - max_actions: ?usize, -) !void { - try performDbAction( - "RocksDb.count", - BlockstoreDB.count, - .{ db, cf1 }, - max_actions, - ); + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const random_index = random.uintLessThan(usize, dataKeys.items.len); + const startKey = dataKeys.items[random_index]; + const endKey = startKey +| @as(u32, random.int(u8)); + + try BlockstoreDB.deleteFilesInRange(db, cf1, startKey, endKey); + + for (startKey..endKey) |key| { + const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse null; + try std.testing.expectEqual(null, actual); + } + + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; + } } -fn dbContains( +fn dbDelete( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { - const key = random.int(u32); - try performDbAction( - "RocksDb.contains", - BlockstoreDB.contains, - .{ db, cf1, key }, - max_actions, - ); + var count: u64 = 0; + var last_print_msg_count: u64 = 0; + const action_name = "delete"; + + while (true) { + if (max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + break; + } + } + + const random_index = random.uintLessThan(usize, dataKeys.items.len); + const key = dataKeys.items[random_index]; + + try BlockstoreDB.delete(db, cf1, key); + + const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse null; + try std.testing.expectEqual(null, actual); + + if ((count - last_print_msg_count) >= 1_000) { + std.debug.print("{d} {s} actions\n", .{ count, action_name }); + last_print_msg_count = count; + } + + count += 1; + } } // Batch API -fn batchDeleteRange( +fn batchOps( db: *BlockstoreDB, random: *const std.rand.Random, max_actions: ?usize, ) !void { + // Repurpose the gloabl map. + dataMap.clearAndFree(); + var count: u64 = 0; var last_print_msg_count: u64 = 0; while (true) { @@ -231,32 +342,91 @@ fn batchDeleteRange( break; } } - const start = random.int(u32); - const end = blk: { - const end_ = random.int(u32); - if (end_ < start) - break :blk (end_ +| start) - else - break :blk end_; - }; - - const key = random.int(u32); - var buffer: [61]u8 = undefined; - // Fill the buffer with random bytes - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); + // Batch put + { + const startKey = random.int(u32); + const endKey = startKey +| @as(u32, random.int(u8)); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + defer dataMap.clearAndFree(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try batch.put(cf1, key, data); + try dataMap.put(@as(u32, @intCast(key)), data); + } + // Commit batch put. + try db.commit(&batch); + var it = dataMap.iterator(); + while (it.next()) |entry| { + const entryKey = entry.key_ptr.*; + const expected = entry.value_ptr.*; + const actual = try BlockstoreDB.get(db, allocator, cf1, entryKey) orelse return error.KeyNotFoundError; + try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); + } } - const value: []const u8 = buffer[0..]; - - var batch = try db.initWriteBatch(); - defer batch.deinit(); + // Batch delete. + { + const startKey = random.int(u32); + const endKey = startKey +| @as(u32, random.int(u8)); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try batch.put(cf1, key, data); + try batch.delete(cf1, key); + } + // Commit batch put and delete. + try db.commit(&batch); + for (startKey..endKey) |key| { + const actual = try BlockstoreDB.get(db, allocator, cf1, @as(u32, @intCast(key))); + try std.testing.expectEqual(null, actual); + } + } - try batch.put(cf1, key, Data{ .value = value }); - try batch.deleteRange(cf1, start, end); - try batch.delete(cf1, key); - try db.commit(&batch); + // Batch delete range. + { + const startKey = random.int(u32); + const endKey = startKey +| @as(u32, random.int(u8)); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try batch.put(cf1, key, data); + } + try batch.deleteRange(cf1, startKey, endKey); + // Commit batch put and delete range. + try db.commit(&batch); + for (startKey..endKey) |key| { + const actual = try BlockstoreDB.get(db, allocator, cf1, @as(u32, @intCast(key))); + try std.testing.expectEqual(null, actual); + } + } if ((count - last_print_msg_count) >= 1_000) { std.debug.print("{d} Batch actions\n", .{count}); From 3e07071e964b0ed5a4f31f7aa7e0345285187829 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:45:14 +0000 Subject: [PATCH 09/25] Pass random as value --- src/ledger/fuzz.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index b2bfb5359..9adcfe49b 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -75,14 +75,14 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { }; inline for (functions) |function| { - const fn_args = .{ &db, &random, maybe_max_actions }; + const fn_args = .{ &db, random, maybe_max_actions }; _ = try @call(.auto, function, fn_args); } } fn dbPut( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -124,7 +124,7 @@ fn dbPut( fn dbGet( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -157,7 +157,7 @@ fn dbGet( fn dbGetBytes( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -193,7 +193,7 @@ fn dbCount( db: *BlockstoreDB, // Unused. Listed to allow uniform call // via @call with the rest of the functions. - _: *const std.rand.Random, + _: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -223,7 +223,7 @@ fn dbCount( fn dbContains( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -255,7 +255,7 @@ fn dbContains( fn dbDeleteFilesInRange( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -292,7 +292,7 @@ fn dbDeleteFilesInRange( fn dbDelete( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { var count: u64 = 0; @@ -327,7 +327,7 @@ fn dbDelete( // Batch API fn batchOps( db: *BlockstoreDB, - random: *const std.rand.Random, + random: std.rand.Random, max_actions: ?usize, ) !void { // Repurpose the gloabl map. From 3917f2c17248298911e5ec4c65506b8668ba14dd Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:52:12 +0000 Subject: [PATCH 10/25] Added dbDeleteFilesInRange --- src/ledger/fuzz.zig | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 9adcfe49b..f918df135 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -25,6 +25,23 @@ pub const BlockstoreDB = switch (build_options.blockstore_db) { .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; +fn createBlockstoreDB() !BlockstoreDB { + const rocksdb_path = + try std.fmt.allocPrint(allocator, "{s}/ledger/rocksdb", .{sig.FUZZ_DATA_DIR}); + + // ensure we start with a clean slate. + if (std.fs.cwd().access(rocksdb_path, .{})) |_| { + try std.fs.cwd().deleteTree(rocksdb_path); + } else |_| {} + try std.fs.cwd().makePath(rocksdb_path); + + return try BlockstoreDB.open( + allocator, + .noop, + rocksdb_path, + ); +} + pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const maybe_max_actions_string = args.next(); const maybe_max_actions = blk: { @@ -35,13 +52,6 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } }; - // NOTE: change to trace for full logs - var std_logger = sig.trace.DirectPrintLogger.init( - allocator, - .debug, - ); - const logger = std_logger.logger(); - var prng = std.rand.DefaultPrng.init(seed); const random = prng.random(); @@ -54,11 +64,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } else |_| {} try std.fs.cwd().makePath(rocksdb_path); - var db = try BlockstoreDB.open( - allocator, - logger, - rocksdb_path, - ); + var db = try createBlockstoreDB(); defer db.deinit(); @@ -69,7 +75,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { // dbCount, dbContains, dbDelete, - // dbDeleteFilesInRange, + dbDeleteFilesInRange, // Batch API batchOps, }; @@ -110,7 +116,6 @@ fn dbPut( try BlockstoreDB.put(db, cf1, key, data); try dataMap.put(key, data); - std.debug.print("insert key {d}, max {d}\n", .{ key, std.math.maxInt(u32) }); try dataKeys.append(key); if ((count - last_print_msg_count) >= 1_000) { @@ -258,6 +263,10 @@ fn dbDeleteFilesInRange( random: std.rand.Random, max_actions: ?usize, ) !void { + // deleteFilesInRange is not implemented in hashmap implementation. + if (build_options.blockstore_db == .hashmap) { + return; + } var count: u64 = 0; var last_print_msg_count: u64 = 0; const action_name = "deleteFilesInRange"; @@ -275,6 +284,10 @@ fn dbDeleteFilesInRange( const endKey = startKey +| @as(u32, random.int(u8)); try BlockstoreDB.deleteFilesInRange(db, cf1, startKey, endKey); + // Need to flush memtable to disk to be able to see result of deleteFilesInRange. + // We do that by deiniting the current db, which triggers the flushing. + db.deinit(); + db.* = try createBlockstoreDB(); for (startKey..endKey) |key| { const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse null; From 50fdff82fb24f9db2ba67ca9f2bcaf7a55bc1c73 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:35:45 +0000 Subject: [PATCH 11/25] Use method syntax --- src/ledger/fuzz.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index f918df135..d3f5b32d8 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -114,7 +114,7 @@ fn dbPut( const value: []const u8 = try allocator.dupe(u8, buffer[0..]); const data = Data{ .value = value }; - try BlockstoreDB.put(db, cf1, key, data); + try db.put(cf1, key, data); try dataMap.put(key, data); try dataKeys.append(key); @@ -148,7 +148,7 @@ fn dbGet( const key = dataKeys.items[random_index]; const expected = dataMap.get(key) orelse return error.KeyNotFoundError; - const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse return error.KeyNotFoundError; + const actual = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError; try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); if ((count - last_print_msg_count) >= 1_000) { @@ -181,7 +181,7 @@ fn dbGetBytes( const key = dataKeys.items[random_index]; const expected = dataMap.get(key) orelse return error.KeyNotFoundError; - const actualBytes = try BlockstoreDB.getBytes(db, cf1, key) orelse return error.KeyNotFoundError; + const actualBytes = try db.getBytes(cf1, key) orelse return error.KeyNotFoundError; const actual = try ledger.database.value_serializer.deserialize(cf1.Value, allocator, actualBytes.data); try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); @@ -214,7 +214,7 @@ fn dbCount( } const expected = dataKeys.items.len; - const actual = try BlockstoreDB.count(db, cf1); + const actual = try db.count(cf1); try std.testing.expectEqual(expected, actual); if ((count - last_print_msg_count) >= 1_000) { @@ -246,7 +246,7 @@ fn dbContains( const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; - const actual = try BlockstoreDB.contains(db, cf1, key); + const actual = try db.contains(cf1, key); try std.testing.expect(actual); if ((count - last_print_msg_count) >= 1_000) { @@ -283,14 +283,14 @@ fn dbDeleteFilesInRange( const startKey = dataKeys.items[random_index]; const endKey = startKey +| @as(u32, random.int(u8)); - try BlockstoreDB.deleteFilesInRange(db, cf1, startKey, endKey); + try db.deleteFilesInRange(cf1, startKey, endKey); // Need to flush memtable to disk to be able to see result of deleteFilesInRange. // We do that by deiniting the current db, which triggers the flushing. db.deinit(); db.* = try createBlockstoreDB(); for (startKey..endKey) |key| { - const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse null; + const actual = try db.get(allocator, cf1, key) orelse null; try std.testing.expectEqual(null, actual); } @@ -323,9 +323,9 @@ fn dbDelete( const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; - try BlockstoreDB.delete(db, cf1, key); + try db.delete(cf1, key); - const actual = try BlockstoreDB.get(db, allocator, cf1, key) orelse null; + const actual = try db.get(allocator, cf1, key) orelse null; try std.testing.expectEqual(null, actual); if ((count - last_print_msg_count) >= 1_000) { @@ -382,7 +382,7 @@ fn batchOps( while (it.next()) |entry| { const entryKey = entry.key_ptr.*; const expected = entry.value_ptr.*; - const actual = try BlockstoreDB.get(db, allocator, cf1, entryKey) orelse return error.KeyNotFoundError; + const actual = try db.get(allocator, cf1, entryKey) orelse return error.KeyNotFoundError; try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } } @@ -409,7 +409,7 @@ fn batchOps( // Commit batch put and delete. try db.commit(&batch); for (startKey..endKey) |key| { - const actual = try BlockstoreDB.get(db, allocator, cf1, @as(u32, @intCast(key))); + const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); try std.testing.expectEqual(null, actual); } } @@ -436,7 +436,7 @@ fn batchOps( // Commit batch put and delete range. try db.commit(&batch); for (startKey..endKey) |key| { - const actual = try BlockstoreDB.get(db, allocator, cf1, @as(u32, @intCast(key))); + const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); try std.testing.expectEqual(null, actual); } } From 5c3a5f96c5b50691cfe36188277f500f2b3ddf15 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:19:02 +0000 Subject: [PATCH 12/25] Skip count for rocksdb impl --- src/ledger/fuzz.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index d3f5b32d8..a2a7b2ea4 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -72,7 +72,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { dbPut, dbGet, dbGetBytes, - // dbCount, + dbCount, dbContains, dbDelete, dbDeleteFilesInRange, @@ -201,6 +201,12 @@ fn dbCount( _: std.rand.Random, max_actions: ?usize, ) !void { + // TODO Fix why changes are not reflected in count with rocksdb implementation, + // but it does with hashmap. + if (build_options.blockstore_db == .rocksdb) { + return; + } + var count: u64 = 0; var last_print_msg_count: u64 = 0; const action_name = "count"; From db3c374e5c1094e39e18e00a24bd8b3fcaa34a05 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:20:54 +0000 Subject: [PATCH 13/25] Rename batchOps to BatchAPI --- src/ledger/fuzz.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index a2a7b2ea4..58fd8be6f 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -77,7 +77,10 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { dbDelete, dbDeleteFilesInRange, // Batch API - batchOps, + // - batch.put + // - batch.delete + // - batch.deleteRange + batchAPI, }; inline for (functions) |function| { @@ -344,7 +347,7 @@ fn dbDelete( } // Batch API -fn batchOps( +fn batchAPI( db: *BlockstoreDB, random: std.rand.Random, max_actions: ?usize, From 0a0ac38a4371c520eefd60f6dfec60c5d9c1eef4 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:24:44 +0000 Subject: [PATCH 14/25] Fix style --- src/ledger/fuzz.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 58fd8be6f..036fd8e3b 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -185,7 +185,11 @@ fn dbGetBytes( const expected = dataMap.get(key) orelse return error.KeyNotFoundError; const actualBytes = try db.getBytes(cf1, key) orelse return error.KeyNotFoundError; - const actual = try ledger.database.value_serializer.deserialize(cf1.Value, allocator, actualBytes.data); + const actual = try ledger.database.value_serializer.deserialize( + cf1.Value, + allocator, + actualBytes.data, + ); try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); if ((count - last_print_msg_count) >= 1_000) { @@ -391,7 +395,11 @@ fn batchAPI( while (it.next()) |entry| { const entryKey = entry.key_ptr.*; const expected = entry.value_ptr.*; - const actual = try db.get(allocator, cf1, entryKey) orelse return error.KeyNotFoundError; + const actual = try db.get( + allocator, + cf1, + entryKey, + ) orelse return error.KeyNotFoundError; try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } } From 9cfe206d3892210badb757d709a503a1f37e6f32 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:36:36 +0000 Subject: [PATCH 15/25] Use if as expression --- src/ledger/fuzz.zig | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 036fd8e3b..f9dbffe86 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -44,13 +44,11 @@ fn createBlockstoreDB() !BlockstoreDB { pub fn run(seed: u64, args: *std.process.ArgIterator) !void { const maybe_max_actions_string = args.next(); - const maybe_max_actions = blk: { - if (maybe_max_actions_string) |max_actions_str| { - break :blk try std.fmt.parseInt(usize, max_actions_str, 10); - } else { - break :blk null; - } - }; + + const maybe_max_actions = if (maybe_max_actions_string) |max_actions_str| + try std.fmt.parseInt(usize, max_actions_str, 10) + else + null; var prng = std.rand.DefaultPrng.init(seed); const random = prng.random(); From 076d4b323de44d8407ebaa3520d824a53db535de Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:42:24 +0000 Subject: [PATCH 16/25] Remove explicit reference to rocksdb --- src/ledger/fuzz.zig | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index f9dbffe86..0e8139f1d 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -26,19 +26,19 @@ pub const BlockstoreDB = switch (build_options.blockstore_db) { }; fn createBlockstoreDB() !BlockstoreDB { - const rocksdb_path = - try std.fmt.allocPrint(allocator, "{s}/ledger/rocksdb", .{sig.FUZZ_DATA_DIR}); + const ledger_path = + try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); // ensure we start with a clean slate. - if (std.fs.cwd().access(rocksdb_path, .{})) |_| { - try std.fs.cwd().deleteTree(rocksdb_path); + if (std.fs.cwd().access(ledger_path, .{})) |_| { + try std.fs.cwd().deleteTree(ledger_path); } else |_| {} - try std.fs.cwd().makePath(rocksdb_path); + try std.fs.cwd().makePath(ledger_path); return try BlockstoreDB.open( allocator, .noop, - rocksdb_path, + ledger_path, ); } @@ -53,14 +53,14 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { var prng = std.rand.DefaultPrng.init(seed); const random = prng.random(); - const rocksdb_path = - try std.fmt.allocPrint(allocator, "{s}/ledger/rocksdb", .{sig.FUZZ_DATA_DIR}); + const ledger_path = + try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); // ensure we start with a clean slate. - if (std.fs.cwd().access(rocksdb_path, .{})) |_| { - try std.fs.cwd().deleteTree(rocksdb_path); + if (std.fs.cwd().access(ledger_path, .{})) |_| { + try std.fs.cwd().deleteTree(ledger_path); } else |_| {} - try std.fs.cwd().makePath(rocksdb_path); + try std.fs.cwd().makePath(ledger_path); var db = try createBlockstoreDB(); From c695ce776cf0e94aa2a502233de86c3696196a62 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Sun, 12 Jan 2025 18:03:56 +0000 Subject: [PATCH 17/25] Randomize method calls --- src/ledger/fuzz.zig | 474 +++++++++++++++++--------------------------- 1 file changed, 180 insertions(+), 294 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 0e8139f1d..8eb66c153 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -18,13 +18,32 @@ const cf1 = ColumnFamily{ }; var dataMap = std.AutoHashMap(u32, Data).init(allocator); -var dataKeys = std.ArrayList(u32).init(allocator); +var executed_actions = std.AutoHashMap(Actions, void).init(allocator); pub const BlockstoreDB = switch (build_options.blockstore_db) { .rocksdb => ledger.database.RocksDB(&.{cf1}), .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; +const Actions = enum { + put, + get, + get_bytes, + count, + contains, + delete, + batch, +}; + +fn getKeys(map: *std.AutoHashMap(u32, Data)) !std.ArrayList(u32) { + var keys = std.ArrayList(u32).init(allocator); + var it = map.iterator(); + while (it.next()) |entry| { + try keys.append(entry.key_ptr.*); + } + return keys; +} + fn createBlockstoreDB() !BlockstoreDB { const ledger_path = try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); @@ -66,85 +85,75 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { defer db.deinit(); - const functions = .{ - dbPut, - dbGet, - dbGetBytes, - dbCount, - dbContains, - dbDelete, - dbDeleteFilesInRange, - // Batch API - // - batch.put - // - batch.delete - // - batch.deleteRange - batchAPI, - }; - - inline for (functions) |function| { - const fn_args = .{ &db, random, maybe_max_actions }; - _ = try @call(.auto, function, fn_args); - } -} - -fn dbPut( - db: *BlockstoreDB, - random: std.rand.Random, - max_actions: ?usize, -) !void { var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "put"; while (true) { - if (max_actions) |max| { + if (maybe_max_actions) |max| { if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); + std.debug.print("{s} reached max actions: {}\n", .{ "action_name", max }); break; } } - const key = random.int(u32); - var buffer: [61]u8 = undefined; - - // Fill the buffer with random bytes - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); + const action = random.enumValue(enum { + put, + get, + get_bytes, + count, + contains, + delete, + batch, + }); + + switch (action) { + .put => try dbPut(&db, random), + .get => try dbGet(&db, random), + .get_bytes => try dbGetBytes(&db, random), + .count => try dbCount(&db), + .contains => try dbContains(&db, random), + .delete => try dbDelete(&db, random), + .batch => try batchAPI(&db, random), } - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; - - try db.put(cf1, key, data); - try dataMap.put(key, data); - try dataKeys.append(key); + count += 1; + } - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; + inline for (@typeInfo(Actions).Enum.fields) |field| { + const variant = @field(Actions, field.name); + if (!executed_actions.contains(variant)) { + std.debug.print("Action: '{s}' not executed by the fuzzer", .{@tagName(variant)}); + return error.NonExhaustive; } - - count += 1; } } -fn dbGet( +fn dbPut( db: *BlockstoreDB, random: std.rand.Random, - max_actions: ?usize, ) !void { - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "get"; + try executed_actions.put(Actions.put, void{}); + const key = random.int(u32); + var buffer: [61]u8 = undefined; - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } + // Fill the buffer with random bytes + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); + } + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try db.put(cf1, key, data); + try dataMap.put(key, data); +} + +fn dbGet( + db: *BlockstoreDB, + random: std.rand.Random, +) !void { + try executed_actions.put(Actions.get, void{}); + const dataKeys = try getKeys(&dataMap); + if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; const expected = dataMap.get(key) orelse return error.KeyNotFoundError; @@ -152,32 +161,21 @@ fn dbGet( const actual = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError; try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; + } else { + // If there are no keys, we should get a null value. + const key = random.int(u32); + const actual = try db.get(allocator, cf1, key); + try std.testing.expectEqual(null, actual); } } fn dbGetBytes( db: *BlockstoreDB, random: std.rand.Random, - max_actions: ?usize, ) !void { - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "getBytes"; - - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } - + try executed_actions.put(Actions.get_bytes, void{}); + const dataKeys = try getKeys(&dataMap); + if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; const expected = dataMap.get(key) orelse return error.KeyNotFoundError; @@ -190,147 +188,58 @@ fn dbGetBytes( ); try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; + } else { + // If there are no keys, we should get a null value. + const key = random.int(u32); + const actual = try db.getBytes(cf1, key); + try std.testing.expectEqual(null, actual); } } fn dbCount( db: *BlockstoreDB, - // Unused. Listed to allow uniform call - // via @call with the rest of the functions. - _: std.rand.Random, - max_actions: ?usize, ) !void { + try executed_actions.put(Actions.count, void{}); // TODO Fix why changes are not reflected in count with rocksdb implementation, // but it does with hashmap. if (build_options.blockstore_db == .rocksdb) { return; } - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "count"; - - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } - - const expected = dataKeys.items.len; - const actual = try db.count(cf1); + const expected = dataMap.count(); + const actual = try db.count(cf1); - try std.testing.expectEqual(expected, actual); - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; - } + try std.testing.expectEqual(expected, actual); } fn dbContains( db: *BlockstoreDB, random: std.rand.Random, - max_actions: ?usize, ) !void { - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "contains"; - - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } - + try executed_actions.put(Actions.contains, void{}); + const dataKeys = try getKeys(&dataMap); + if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; const actual = try db.contains(cf1, key); try std.testing.expect(actual); - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; - } -} - -fn dbDeleteFilesInRange( - db: *BlockstoreDB, - random: std.rand.Random, - max_actions: ?usize, -) !void { - // deleteFilesInRange is not implemented in hashmap implementation. - if (build_options.blockstore_db == .hashmap) { - return; - } - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "deleteFilesInRange"; - - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } - - const random_index = random.uintLessThan(usize, dataKeys.items.len); - const startKey = dataKeys.items[random_index]; - const endKey = startKey +| @as(u32, random.int(u8)); - - try db.deleteFilesInRange(cf1, startKey, endKey); - // Need to flush memtable to disk to be able to see result of deleteFilesInRange. - // We do that by deiniting the current db, which triggers the flushing. - db.deinit(); - db.* = try createBlockstoreDB(); - - for (startKey..endKey) |key| { - const actual = try db.get(allocator, cf1, key) orelse null; - try std.testing.expectEqual(null, actual); - } - - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; + } else { + // If there are no keys, we should get a null value. + const key = random.int(u32); + const actual = try db.contains(cf1, key); + try std.testing.expect(!actual); } } fn dbDelete( db: *BlockstoreDB, random: std.rand.Random, - max_actions: ?usize, ) !void { - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - const action_name = "delete"; - - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ action_name, max }); - break; - } - } - + try executed_actions.put(Actions.delete, void{}); + const dataKeys = try getKeys(&dataMap); + if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -338,13 +247,12 @@ fn dbDelete( const actual = try db.get(allocator, cf1, key) orelse null; try std.testing.expectEqual(null, actual); - - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} {s} actions\n", .{ count, action_name }); - last_print_msg_count = count; - } - - count += 1; + // Remove the keys from the global map. + _ = dataMap.remove(key); + } else { + // If there are no keys, we should get a null value. + const key = random.int(u32); + try db.delete(cf1, key); } } @@ -352,115 +260,93 @@ fn dbDelete( fn batchAPI( db: *BlockstoreDB, random: std.rand.Random, - max_actions: ?usize, ) !void { - // Repurpose the gloabl map. - dataMap.clearAndFree(); - - var count: u64 = 0; - var last_print_msg_count: u64 = 0; - while (true) { - if (max_actions) |max| { - if (count >= max) { - std.debug.print("Batch actions reached max actions: {}\n", .{max}); - break; + try executed_actions.put(Actions.batch, void{}); + // Batch put + { + const startKey = random.int(u32); + const endKey = startKey +| random.int(u8); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); } - } - // Batch put - { - const startKey = random.int(u32); - const endKey = startKey +| @as(u32, random.int(u8)); - var buffer: [61]u8 = undefined; - var batch = try db.initWriteBatch(); - defer batch.deinit(); - defer dataMap.clearAndFree(); - for (startKey..endKey) |key| { - // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; - - try batch.put(cf1, key, data); - try dataMap.put(@as(u32, @intCast(key)), data); - } - // Commit batch put. - try db.commit(&batch); - var it = dataMap.iterator(); - while (it.next()) |entry| { - const entryKey = entry.key_ptr.*; - const expected = entry.value_ptr.*; - const actual = try db.get( - allocator, - cf1, - entryKey, - ) orelse return error.KeyNotFoundError; - try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); - } + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try batch.put(cf1, key, data); + try dataMap.put(@as(u32, @intCast(key)), data); + } + // Commit batch put. + try db.commit(&batch); + var it = dataMap.iterator(); + while (it.next()) |entry| { + const entryKey = entry.key_ptr.*; + const expected = entry.value_ptr.*; + const actual = try db.get( + allocator, + cf1, + entryKey, + ) orelse return error.KeyNotFoundError; + try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } + } - // Batch delete. - { - const startKey = random.int(u32); - const endKey = startKey +| @as(u32, random.int(u8)); - var buffer: [61]u8 = undefined; - var batch = try db.initWriteBatch(); - defer batch.deinit(); - for (startKey..endKey) |key| { - // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; - - try batch.put(cf1, key, data); - try batch.delete(cf1, key); - } - // Commit batch put and delete. - try db.commit(&batch); - for (startKey..endKey) |key| { - const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); - try std.testing.expectEqual(null, actual); + // Batch delete. + { + const startKey = random.int(u32); + const endKey = startKey +| random.int(u8); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); } + + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; + + try batch.put(cf1, key, data); + try batch.delete(cf1, key); } + // Commit batch put and delete. + try db.commit(&batch); + for (startKey..endKey) |key| { + const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); + try std.testing.expectEqual(null, actual); + } + } - // Batch delete range. - { - const startKey = random.int(u32); - const endKey = startKey +| @as(u32, random.int(u8)); - var buffer: [61]u8 = undefined; - var batch = try db.initWriteBatch(); - defer batch.deinit(); - for (startKey..endKey) |key| { - // Fill the buffer with random bytes for each key. - for (0..buffer.len) |i| { - buffer[i] = @intCast(random.int(u8)); - } - - const value: []const u8 = try allocator.dupe(u8, buffer[0..]); - const data = Data{ .value = value }; - - try batch.put(cf1, key, data); - } - try batch.deleteRange(cf1, startKey, endKey); - // Commit batch put and delete range. - try db.commit(&batch); - for (startKey..endKey) |key| { - const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); - try std.testing.expectEqual(null, actual); + // Batch delete range. + { + const startKey = random.int(u32); + const endKey = startKey +| random.int(u8); + var buffer: [61]u8 = undefined; + var batch = try db.initWriteBatch(); + defer batch.deinit(); + for (startKey..endKey) |key| { + // Fill the buffer with random bytes for each key. + for (0..buffer.len) |i| { + buffer[i] = @intCast(random.int(u8)); } - } - if ((count - last_print_msg_count) >= 1_000) { - std.debug.print("{d} Batch actions\n", .{count}); - last_print_msg_count = count; - } + const value: []const u8 = try allocator.dupe(u8, buffer[0..]); + const data = Data{ .value = value }; - count += 1; + try batch.put(cf1, key, data); + } + try batch.deleteRange(cf1, startKey, endKey); + // Commit batch put and delete range. + try db.commit(&batch); + for (startKey..endKey) |key| { + const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); + try std.testing.expectEqual(null, actual); + } } } From e29fef7149e4106793db48735e2411d147b9fb94 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:21:49 +0000 Subject: [PATCH 18/25] Renamed variable and added documentation --- src/ledger/fuzz.zig | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 8eb66c153..e56fe0bc3 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -17,7 +17,9 @@ const cf1 = ColumnFamily{ .Value = Data, }; -var dataMap = std.AutoHashMap(u32, Data).init(allocator); +// Note: This is a simpler blockstore which is used to make sure +// the method calls being fuzzed return expected data. +var data_map = std.AutoHashMap(u32, Data).init(allocator); var executed_actions = std.AutoHashMap(Actions, void).init(allocator); pub const BlockstoreDB = switch (build_options.blockstore_db) { @@ -144,7 +146,7 @@ fn dbPut( const data = Data{ .value = value }; try db.put(cf1, key, data); - try dataMap.put(key, data); + try data_map.put(key, data); } fn dbGet( @@ -152,11 +154,11 @@ fn dbGet( random: std.rand.Random, ) !void { try executed_actions.put(Actions.get, void{}); - const dataKeys = try getKeys(&dataMap); + const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; - const expected = dataMap.get(key) orelse return error.KeyNotFoundError; + const expected = data_map.get(key) orelse return error.KeyNotFoundError; const actual = try db.get(allocator, cf1, key) orelse return error.KeyNotFoundError; @@ -174,11 +176,11 @@ fn dbGetBytes( random: std.rand.Random, ) !void { try executed_actions.put(Actions.get_bytes, void{}); - const dataKeys = try getKeys(&dataMap); + const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; - const expected = dataMap.get(key) orelse return error.KeyNotFoundError; + const expected = data_map.get(key) orelse return error.KeyNotFoundError; const actualBytes = try db.getBytes(cf1, key) orelse return error.KeyNotFoundError; const actual = try ledger.database.value_serializer.deserialize( @@ -206,7 +208,7 @@ fn dbCount( return; } - const expected = dataMap.count(); + const expected = data_map.count(); const actual = try db.count(cf1); try std.testing.expectEqual(expected, actual); @@ -217,7 +219,7 @@ fn dbContains( random: std.rand.Random, ) !void { try executed_actions.put(Actions.contains, void{}); - const dataKeys = try getKeys(&dataMap); + const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -238,7 +240,7 @@ fn dbDelete( random: std.rand.Random, ) !void { try executed_actions.put(Actions.delete, void{}); - const dataKeys = try getKeys(&dataMap); + const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -248,7 +250,7 @@ fn dbDelete( const actual = try db.get(allocator, cf1, key) orelse null; try std.testing.expectEqual(null, actual); // Remove the keys from the global map. - _ = dataMap.remove(key); + _ = data_map.remove(key); } else { // If there are no keys, we should get a null value. const key = random.int(u32); @@ -279,11 +281,11 @@ fn batchAPI( const data = Data{ .value = value }; try batch.put(cf1, key, data); - try dataMap.put(@as(u32, @intCast(key)), data); + try data_map.put(@as(u32, @intCast(key)), data); } // Commit batch put. try db.commit(&batch); - var it = dataMap.iterator(); + var it = data_map.iterator(); while (it.next()) |entry| { const entryKey = entry.key_ptr.*; const expected = entry.value_ptr.*; From a84612b1b64b4a763ed0b5843a76bb7c3bbbe9cf Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:24:41 +0000 Subject: [PATCH 19/25] Added comments explaining why deleteFilesInRange is not included --- src/ledger/fuzz.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index e56fe0bc3..491ab76f1 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -27,6 +27,9 @@ pub const BlockstoreDB = switch (build_options.blockstore_db) { .hashmap => ledger.database.SharedHashMapDB(&.{cf1}), }; +// Note: deleteFilesInRange is not included in the fuzzing as it is not +// implemented in the hashmap implementation, and the RocksDB implementation +// requires manual flushing of the memtable to disk to make the changes visible. const Actions = enum { put, get, From 0688ef9b24d019d4b10ab23d0ccdebcdf0d9a7dd Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:09:14 +0000 Subject: [PATCH 20/25] Comment --- src/ledger/fuzz.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 491ab76f1..22ecf749e 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -287,6 +287,7 @@ fn batchAPI( try data_map.put(@as(u32, @intCast(key)), data); } // Commit batch put. + // Note: Returns void so no confirmation needed. try db.commit(&batch); var it = data_map.iterator(); while (it.next()) |entry| { @@ -321,6 +322,7 @@ fn batchAPI( try batch.delete(cf1, key); } // Commit batch put and delete. + // Note: Returns void so no confirmation needed. try db.commit(&batch); for (startKey..endKey) |key| { const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); @@ -348,6 +350,7 @@ fn batchAPI( } try batch.deleteRange(cf1, startKey, endKey); // Commit batch put and delete range. + // Note: Returns void so no confirmation needed. try db.commit(&batch); for (startKey..endKey) |key| { const actual = try db.get(allocator, cf1, @as(u32, @intCast(key))); From de89d3cf812dd504412c882e4977937f8c8b3795 Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:10:13 +0000 Subject: [PATCH 21/25] Use the defined enum --- src/ledger/fuzz.zig | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 22ecf749e..fe6fc76e9 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -100,15 +100,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } } - const action = random.enumValue(enum { - put, - get, - get_bytes, - count, - contains, - delete, - batch, - }); + const action = random.enumValue(Actions); switch (action) { .put => try dbPut(&db, random), From ce9f552b31caf412b89107261370b5a9a821050e Mon Sep 17 00:00:00 2001 From: Dade <272535+dadepo@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:13:09 +0000 Subject: [PATCH 22/25] Use just {} to represent the void value --- src/ledger/fuzz.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index fe6fc76e9..7576427c2 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -128,7 +128,7 @@ fn dbPut( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.put, void{}); + try executed_actions.put(Actions.put, {}); const key = random.int(u32); var buffer: [61]u8 = undefined; @@ -148,7 +148,7 @@ fn dbGet( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.get, void{}); + try executed_actions.put(Actions.get, {}); const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); @@ -170,7 +170,7 @@ fn dbGetBytes( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.get_bytes, void{}); + try executed_actions.put(Actions.get_bytes, {}); const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); @@ -196,7 +196,7 @@ fn dbGetBytes( fn dbCount( db: *BlockstoreDB, ) !void { - try executed_actions.put(Actions.count, void{}); + try executed_actions.put(Actions.count, {}); // TODO Fix why changes are not reflected in count with rocksdb implementation, // but it does with hashmap. if (build_options.blockstore_db == .rocksdb) { @@ -213,7 +213,7 @@ fn dbContains( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.contains, void{}); + try executed_actions.put(Actions.contains, {}); const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); @@ -234,7 +234,7 @@ fn dbDelete( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.delete, void{}); + try executed_actions.put(Actions.delete, {}); const dataKeys = try getKeys(&data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); @@ -258,7 +258,7 @@ fn batchAPI( db: *BlockstoreDB, random: std.rand.Random, ) !void { - try executed_actions.put(Actions.batch, void{}); + try executed_actions.put(Actions.batch, {}); // Batch put { const startKey = random.int(u32); From 26df2aa70d5d129c6a1478cfb5fad2c35f72187a Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 15 Jan 2025 12:29:32 -0300 Subject: [PATCH 23/25] test(ledger): discrete fuzzer test cases --- src/ledger/fuzz.zig | 68 ++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 7576427c2..541d143da 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -17,9 +17,6 @@ const cf1 = ColumnFamily{ .Value = Data, }; -// Note: This is a simpler blockstore which is used to make sure -// the method calls being fuzzed return expected data. -var data_map = std.AutoHashMap(u32, Data).init(allocator); var executed_actions = std.AutoHashMap(Actions, void).init(allocator); pub const BlockstoreDB = switch (build_options.blockstore_db) { @@ -66,7 +63,8 @@ fn createBlockstoreDB() !BlockstoreDB { ); } -pub fn run(seed: u64, args: *std.process.ArgIterator) !void { +pub fn run(initial_seed: u64, args: *std.process.ArgIterator) !void { + var seed = initial_seed; const maybe_max_actions_string = args.next(); const maybe_max_actions = if (maybe_max_actions_string) |max_actions_str| @@ -74,9 +72,6 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { else null; - var prng = std.rand.DefaultPrng.init(seed); - const random = prng.random(); - const ledger_path = try std.fmt.allocPrint(allocator, "{s}/ledger", .{sig.FUZZ_DATA_DIR}); @@ -92,27 +87,37 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { var count: u64 = 0; - while (true) { - if (maybe_max_actions) |max| { - if (count >= max) { - std.debug.print("{s} reached max actions: {}\n", .{ "action_name", max }); - break; + while (true) outer: { + var prng = std.rand.DefaultPrng.init(seed); + const random = prng.random(); + // This is a simpler blockstore which is used to make sure + // the method calls being fuzzed return expected data. + var data_map = std.AutoHashMap(u32, Data).init(allocator); + defer data_map.deinit(); + for (0..1_000) |_| { + if (maybe_max_actions) |max| { + if (count >= max) { + std.debug.print("{s} reached max actions: {}\n", .{ "action_name", max }); + break :outer; + } } - } - const action = random.enumValue(Actions); + const action = random.enumValue(Actions); - switch (action) { - .put => try dbPut(&db, random), - .get => try dbGet(&db, random), - .get_bytes => try dbGetBytes(&db, random), - .count => try dbCount(&db), - .contains => try dbContains(&db, random), - .delete => try dbDelete(&db, random), - .batch => try batchAPI(&db, random), - } + switch (action) { + .put => try dbPut(&data_map, &db, random), + .get => try dbGet(&data_map, &db, random), + .get_bytes => try dbGetBytes(&data_map, &db, random), + .count => try dbCount(&data_map, &db), + .contains => try dbContains(&data_map, &db, random), + .delete => try dbDelete(&data_map, &db, random), + .batch => try batchAPI(&data_map, &db, random), + } - count += 1; + count += 1; + } + seed += 1; + std.debug.print("using seed: {}\n", .{seed}); } inline for (@typeInfo(Actions).Enum.fields) |field| { @@ -125,6 +130,7 @@ pub fn run(seed: u64, args: *std.process.ArgIterator) !void { } fn dbPut( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { @@ -145,11 +151,12 @@ fn dbPut( } fn dbGet( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { try executed_actions.put(Actions.get, {}); - const dataKeys = try getKeys(&data_map); + const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -167,11 +174,12 @@ fn dbGet( } fn dbGetBytes( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { try executed_actions.put(Actions.get_bytes, {}); - const dataKeys = try getKeys(&data_map); + const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -194,6 +202,7 @@ fn dbGetBytes( } fn dbCount( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, ) !void { try executed_actions.put(Actions.count, {}); @@ -210,11 +219,12 @@ fn dbCount( } fn dbContains( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { try executed_actions.put(Actions.contains, {}); - const dataKeys = try getKeys(&data_map); + const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -231,11 +241,12 @@ fn dbContains( } fn dbDelete( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { try executed_actions.put(Actions.delete, {}); - const dataKeys = try getKeys(&data_map); + const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -255,6 +266,7 @@ fn dbDelete( // Batch API fn batchAPI( + data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.rand.Random, ) !void { From c797b8a5d6d651e7d4157feed4a166fc32de0e02 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 15 Jan 2025 12:36:01 -0300 Subject: [PATCH 24/25] use std.Random not std.rand --- src/ledger/fuzz.zig | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 541d143da..3a3c6897d 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -88,7 +88,7 @@ pub fn run(initial_seed: u64, args: *std.process.ArgIterator) !void { var count: u64 = 0; while (true) outer: { - var prng = std.rand.DefaultPrng.init(seed); + var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); // This is a simpler blockstore which is used to make sure // the method calls being fuzzed return expected data. @@ -129,11 +129,7 @@ pub fn run(initial_seed: u64, args: *std.process.ArgIterator) !void { } } -fn dbPut( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn dbPut(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.put, {}); const key = random.int(u32); var buffer: [61]u8 = undefined; @@ -150,11 +146,7 @@ fn dbPut( try data_map.put(key, data); } -fn dbGet( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.get, {}); const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { @@ -173,11 +165,7 @@ fn dbGet( } } -fn dbGetBytes( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn dbGetBytes(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.get_bytes, {}); const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { @@ -218,11 +206,7 @@ fn dbCount( try std.testing.expectEqual(expected, actual); } -fn dbContains( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.contains, {}); const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { @@ -240,11 +224,7 @@ fn dbContains( } } -fn dbDelete( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn dbDelete(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.delete, {}); const dataKeys = try getKeys(data_map); if (dataKeys.items.len > 0) { @@ -265,11 +245,7 @@ fn dbDelete( } // Batch API -fn batchAPI( - data_map: *std.AutoHashMap(u32, Data), - db: *BlockstoreDB, - random: std.rand.Random, -) !void { +fn batchAPI(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.batch, {}); // Batch put { From efba45807be2dffee385dcd4ee1fc559f5151565 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 15 Jan 2025 12:43:00 -0300 Subject: [PATCH 25/25] test(ledger): key should sometimes found, sometimes not --- src/ledger/fuzz.zig | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ledger/fuzz.zig b/src/ledger/fuzz.zig index 3a3c6897d..07bda5dfc 100644 --- a/src/ledger/fuzz.zig +++ b/src/ledger/fuzz.zig @@ -149,7 +149,7 @@ fn dbPut(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.R fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.get, {}); const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0) { + if (dataKeys.items.len > 0 and random.boolean()) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; const expected = data_map.get(key) orelse return error.KeyNotFoundError; @@ -159,7 +159,8 @@ fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.R try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } else { // If there are no keys, we should get a null value. - const key = random.int(u32); + var key: u32 = random.int(u32); + while (data_map.contains(key)) key = random.int(u32); const actual = try db.get(allocator, cf1, key); try std.testing.expectEqual(null, actual); } @@ -168,7 +169,7 @@ fn dbGet(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.R fn dbGetBytes(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.get_bytes, {}); const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0) { + if (dataKeys.items.len > 0 and random.boolean()) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; const expected = data_map.get(key) orelse return error.KeyNotFoundError; @@ -183,7 +184,8 @@ fn dbGetBytes(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: try std.testing.expect(std.mem.eql(u8, expected.value, actual.value)); } else { // If there are no keys, we should get a null value. - const key = random.int(u32); + var key: u32 = random.int(u32); + while (data_map.contains(key)) key = random.int(u32); const actual = try db.getBytes(cf1, key); try std.testing.expectEqual(null, actual); } @@ -209,7 +211,7 @@ fn dbCount( fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.contains, {}); const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0) { + if (dataKeys.items.len > 0 and random.boolean()) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -218,7 +220,8 @@ fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: try std.testing.expect(actual); } else { // If there are no keys, we should get a null value. - const key = random.int(u32); + var key: u32 = random.int(u32); + while (data_map.contains(key)) key = random.int(u32); const actual = try db.contains(cf1, key); try std.testing.expect(!actual); } @@ -227,7 +230,7 @@ fn dbContains(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: fn dbDelete(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: std.Random) !void { try executed_actions.put(Actions.delete, {}); const dataKeys = try getKeys(data_map); - if (dataKeys.items.len > 0) { + if (dataKeys.items.len > 0 and random.boolean()) { const random_index = random.uintLessThan(usize, dataKeys.items.len); const key = dataKeys.items[random_index]; @@ -239,7 +242,8 @@ fn dbDelete(data_map: *std.AutoHashMap(u32, Data), db: *BlockstoreDB, random: st _ = data_map.remove(key); } else { // If there are no keys, we should get a null value. - const key = random.int(u32); + var key: u32 = random.int(u32); + while (data_map.contains(key)) key = random.int(u32); try db.delete(cf1, key); } }