From 17070a7c8fdef90260e7979ad5be3838dcf60d01 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 25 Oct 2024 11:36:20 -0400 Subject: [PATCH 01/32] feat(ledger): lmdb wip --- build.zig | 7 + build.zig.zon | 4 + src/ledger/database/lib.zig | 3 +- src/ledger/database/lmdb.zig | 568 +++++++++++++++++++++++++++++++++++ src/utils/allocators.zig | 4 + 5 files changed, 585 insertions(+), 1 deletion(-) create mode 100644 src/ledger/database/lmdb.zig diff --git a/build.zig b/build.zig index e81583449..94d5667c6 100644 --- a/build.zig +++ b/build.zig @@ -42,6 +42,9 @@ pub fn build(b: *Build) void { const rocksdb_dep = b.dependency("rocksdb", dep_opts); const rocksdb_mod = rocksdb_dep.module("rocksdb-bindings"); + const lmdb_dep = b.dependency("lmdb", dep_opts); + const lmdb_mod = lmdb_dep.module("lmdb"); + const pretty_table_dep = b.dependency("prettytable", dep_opts); const pretty_table_mod = pretty_table_dep.module("prettytable"); @@ -56,6 +59,7 @@ pub fn build(b: *Build) void { sig_mod.addImport("zstd", zstd_mod); sig_mod.addImport("curl", curl_mod); sig_mod.addImport("rocksdb", rocksdb_mod); + sig_mod.addImport("lmdb", lmdb_mod); // main executable const sig_exe = b.addExecutable(.{ @@ -73,6 +77,7 @@ pub fn build(b: *Build) void { sig_exe.root_module.addImport("zig-network", zig_network_module); sig_exe.root_module.addImport("zstd", zstd_mod); sig_exe.root_module.addImport("rocksdb", rocksdb_mod); + sig_exe.root_module.addImport("lmdb", lmdb_mod); sig_exe.linkLibC(); const main_exe_run = b.addRunArtifact(sig_exe); @@ -111,6 +116,7 @@ pub fn build(b: *Build) void { unit_tests_exe.root_module.addImport("zig-network", zig_network_module); unit_tests_exe.root_module.addImport("zstd", zstd_mod); unit_tests_exe.root_module.addImport("rocksdb", rocksdb_mod); + unit_tests_exe.root_module.addImport("lmdb", lmdb_mod); unit_tests_exe.linkLibC(); const unit_tests_exe_run = b.addRunArtifact(unit_tests_exe); @@ -151,6 +157,7 @@ pub fn build(b: *Build) void { benchmark_exe.root_module.addImport("httpz", httpz_mod); benchmark_exe.root_module.addImport("zstd", zstd_mod); benchmark_exe.root_module.addImport("rocksdb", rocksdb_mod); + benchmark_exe.root_module.addImport("lmdb", lmdb_mod); benchmark_exe.root_module.addImport("prettytable", pretty_table_mod); benchmark_exe.linkLibC(); diff --git a/build.zig.zon b/build.zig.zon index 42cabeed8..e29c73e33 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -35,6 +35,10 @@ .url = "https://github.com/Syndica/rocksdb-zig/archive/9c09659a5e41f226b6b8f3fa21149247eb26dfae.tar.gz", .hash = "1220aeb80b2f8bb48c131ef306fe48ddfcb537210c5f77742e921cbf40fc4c19b41e", }, + .lmdb = .{ + .url = "https://github.com/Syndica/lmdb-zig/archive/ffa8d332cbe85f05b0e6d20db8764801bc16d1e1.tar.gz", + .hash = "1220e27a4e98d4f93b1f29c7c38985a4818e6cd135525379e23087c20c8de4a3034b", + }, .prettytable = .{ .url = "https://github.com/dying-will-bullet/prettytable-zig/archive/46b6ad9b5970def35fa43c9613cd244f28862fa9.tar.gz", .hash = "122098d444c9c7112c66481e7655bb5389829c67e04b280a029200545e1971187443", diff --git a/src/ledger/database/lib.zig b/src/ledger/database/lib.zig index 48c27b06f..afc48fd7c 100644 --- a/src/ledger/database/lib.zig +++ b/src/ledger/database/lib.zig @@ -1,5 +1,6 @@ -pub const interface = @import("interface.zig"); pub const hashmap = @import("hashmap.zig"); +pub const interface = @import("interface.zig"); +pub const lmdb = @import("lmdb.zig"); pub const rocksdb = @import("rocksdb.zig"); pub const BytesRef = interface.BytesRef; diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig new file mode 100644 index 000000000..2ca89a486 --- /dev/null +++ b/src/ledger/database/lmdb.zig @@ -0,0 +1,568 @@ +const std = @import("std"); +const c = @import("lmdb"); +const sig = @import("../../sig.zig"); +const database = @import("lib.zig"); + +const Allocator = std.mem.Allocator; + +const BytesRef = database.interface.BytesRef; +const ColumnFamily = database.interface.ColumnFamily; +const IteratorDirection = database.interface.IteratorDirection; +const Logger = sig.trace.Logger; +const ReturnType = sig.utils.types.ReturnType; + +const key_serializer = database.interface.key_serializer; +const value_serializer = database.interface.value_serializer; + +pub fn LMDB(comptime column_families: []const ColumnFamily) type { + return struct { + allocator: Allocator, + env: *c.MDB_env, + cf_handles: []const c.MDB_dbi, + path: []const u8, + + const Self = @This(); + + pub fn open(allocator: Allocator, logger: Logger, path: []const u8) LmdbError!Self { + const owned_path = try allocator.dupe(u8, path); + + // create and open the database + const env = try ret(c.mdb_env_create, .{}); + try result(c.mdb_env_set_maxdbs(env, column_families.len)); + try result(c.mdb_env_open(env, path, 0, 0o700)); + + // begin transaction to create column families aka "databases" in lmdb + const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); + errdefer c.mdb_txn_reset(txn); + + // allocate cf handles + const cf_handles = try allocator.alloc(c.MDB_dbi, column_families.len); + errdefer allocator.free(cf_handles); + + // save cf handles + for (column_families, 0..) |cf, i| { + // open cf/database, creating if necessary + cf_handles[i] = try ret(c.mdb_dbi_open, .{ txn, cf.name, 0x40000 }); + } + + // persist column families + try result(c.mdb_txn_commit, .{txn}); + + return .{ + .allocator = allocator, + .db = env, + .logger = logger, + .cf_handles = cf_handles, + .path = owned_path, + }; + } + + pub fn deinit(self: *Self) void { + self.allocator.free(self.cf_handles); + self.db.deinit(); + self.allocator.free(self.path); + } + + pub fn count(self: *Self, comptime cf: ColumnFamily) Allocator.Error!u64 { + const live_files = try self.db.liveFiles(self.allocator); + defer live_files.deinit(); + + var sum: u64 = 0; + for (live_files.items) |live_file| { + if (std.mem.eql(u8, live_file.column_family_name, cf.name)) { + sum += live_file.num_entries; + } + } + + return sum; + } + + pub fn put( + self: *Self, + comptime cf: ColumnFamily, + key: cf.Key, + value: cf.Value, + ) anyerror!void { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + const val_bytes = try value_serializer.serializeToRef(self.allocator, value); + defer val_bytes.deinit(); + + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + errdefer c.mdb_txn_reset(txn); + + const key_val = toVal(key_bytes.data); + const val_val = toVal(val_bytes.data); + try result(c.mdb_put(txn, cf.find(column_families), &key_val, &val_val, 0)); + try result(c.mdb_txn_commit, .{txn}); + } + + pub fn get( + self: *Self, + allocator: Allocator, + comptime cf: ColumnFamily, + key: cf.Key, + ) anyerror!?cf.Value { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + const key_val = toVal(key_bytes); + + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + defer c.mdb_txn_reset(txn); + + const value = try ret(c.mdb_get(txn, cf.find(column_families), &key_val)); + + return try value_serializer.deserialize(cf.Value, allocator, fromVal(value)); + } + + pub fn getBytes(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!?BytesRef { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + const key_val = toVal(key_bytes); + + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + errdefer c.mdb_txn_reset(txn); + + const item = try ret(c.mdb_get(txn, cf.find(column_families), &key_val)); + + return .{ + .allocator = txnResetter(txn), + .data = fromVal(item), + }; + } + + pub fn contains(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!bool { + return try self.getBytes(cf, key) != null; + } + + pub fn delete(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!void { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + const key_val = toVal(key_bytes); + + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + errdefer c.mdb_txn_reset(txn); + + try result(c.mdb_del(txn, cf.find(column_families), &key_val)); + try result(c.mdb_txn_commit(txn)); + } + + pub fn deleteFilesInRange( + self: *Self, + comptime cf: ColumnFamily, + start: cf.Key, + end: cf.Key, + ) anyerror!void { + const start_bytes = try key_serializer.serializeToRef(self.allocator, start); + defer start_bytes.deinit(); + + const end_bytes = try key_serializer.serializeToRef(self.allocator, end); + defer end_bytes.deinit(); + + @panic("TODO"); // TODO + } + + pub fn initWriteBatch(self: *Self) LmdbError!WriteBatch { + return .{ + .allocator = self.allocator, + .inner = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }), + .cf_handles = self.cf_handles, + }; + } + + pub fn commit(_: *Self, batch: WriteBatch) LmdbError!void { + try result(c.mdb_txn_commit(batch.inner)); + } + + /// A write batch is a sequence of operations that execute atomically. + /// This is typically called a "transaction" in most databases. + /// + /// Use this instead of Database.put or Database.delete when you need + /// to ensure that a group of operations are either all executed + /// successfully, or none of them are executed. + /// + /// It is called a write batch instead of a transaction because: + /// - rocksdb uses the name "write batch" for this concept + /// - this name avoids confusion with solana transactions + pub const WriteBatch = struct { + allocator: Allocator, + inner: *c.MDB_txn, + cf_handles: []const c.MDB_dbi, + + pub fn deinit(self: *WriteBatch) void { + self.inner.deinit(); + } + + pub fn put( + self: *WriteBatch, + comptime cf: ColumnFamily, + key: cf.Key, + value: cf.Value, + ) anyerror!void { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + const val_bytes = try value_serializer.serializeToRef(self.allocator, value); + defer val_bytes.deinit(); + + const key_val = toVal(key_bytes.data); + const val_val = toVal(val_bytes.data); + try result(c.mdb_put(self.txn, cf.find(column_families), &key_val, &val_val, 0)); + } + + pub fn delete( + self: *WriteBatch, + comptime cf: ColumnFamily, + key: cf.Key, + ) anyerror!void { + const key_bytes = try key_serializer.serializeToRef(self.allocator, key); + defer key_bytes.deinit(); + self.inner.delete(self.cf_handles[cf.find(column_families)], key_bytes.data); + } + + pub fn deleteRange( + self: *WriteBatch, + comptime cf: ColumnFamily, + start: cf.Key, + end: cf.Key, + ) anyerror!void { + const start_bytes = try key_serializer.serializeToRef(self.allocator, start); + defer start_bytes.deinit(); + + const end_bytes = try key_serializer.serializeToRef(self.allocator, end); + defer end_bytes.deinit(); + + self.inner.deleteRange( + self.cf_handles[cf.find(column_families)], + start_bytes.data, + end_bytes.data, + ); + } + }; + + pub fn iterator( + self: *Self, + comptime cf: ColumnFamily, + comptime direction: IteratorDirection, + start: ?cf.Key, + ) anyerror!Iterator(cf, direction) { + const start_bytes = if (start) |s| try key_serializer.serializeToRef(self.allocator, s) else null; + defer if (start_bytes) |sb| sb.deinit(); + return .{ + .allocator = self.allocator, + .logger = self.logger, + .inner = self.db.iterator( + self.cf_handles[cf.find(column_families)], + switch (direction) { + .forward => .forward, + .reverse => .reverse, + }, + if (start_bytes) |s| s.data else null, + ), + }; + } + + pub fn Iterator(cf: ColumnFamily, _: IteratorDirection) type { + return struct { + allocator: Allocator, + inner: rocks.Iterator, + logger: Logger, + + /// Calling this will free all slices returned by the iterator + pub fn deinit(self: *@This()) void { + self.inner.deinit(); + } + + pub fn next(self: *@This()) anyerror!?cf.Entry() { + const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); + return if (entry) |kv| { + return .{ + try key_serializer.deserialize(cf.Key, self.allocator, kv[0].data), + try value_serializer.deserialize(cf.Value, self.allocator, kv[1].data), + }; + } else null; + } + + pub fn nextKey(self: *@This()) anyerror!?cf.Key { + const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); + return if (entry) |kv| + try key_serializer.deserialize(cf.Key, self.allocator, kv[0].data) + else + null; + } + + pub fn nextValue(self: *@This()) anyerror!?cf.Value { + const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); + return if (entry) |kv| + try key_serializer.deserialize(cf.Value, self.allocator, kv[1].data) + else + null; + } + + /// Returned data does not outlive the iterator. + pub fn nextBytes(self: *@This()) LmdbError!?[2]BytesRef { + const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); + return if (entry) |kv| .{ + .{ .allocator = null, .data = kv[0].data }, + .{ .allocator = null, .data = kv[1].data }, + } else null; + } + }; + } + }; +} + +fn toVal(bytes: []const u8) c.MDB_val { + return .{ + .mv_size = bytes.len, + .mv_data = @constCast(@ptrCast(bytes.ptr)), + }; +} + +fn fromVal(value: [*c]c.MDB_val) []const u8 { + const ptr: [*c]u8 = @ptrCast(value.mv_data); + return ptr[0..value.mv_size]; +} + +fn txnResetter(txn: *c.MDB_txn) Allocator { + return .{ + .ptr = @ptrCast(@alignCast(txn)), + .vtable = .{ + .alloc = &sig.utils.allocators.noAlloc, + .resize = &Allocator.noResize, + .free = &resetTxnFree, + }, + }; +} + +fn resetTxnFree(ctx: *anyopaque, _: []u8, _: u8, _: usize) void { + const txn: *c.MDB_txn = @ptrCast(@alignCast(ctx)); + c.mdb_txn_reset(txn); +} + +fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) { + const Intermediate = IntermediateType(constructor); + var maybe: IntermediateType(constructor) = switch (@typeInfo(Intermediate)) { + .Optional => null, + .Int => 0, + else => undefined, + }; + try result(@call(.auto, constructor, args ++ .{&maybe})); + return switch (@typeInfo(Intermediate)) { + .Optional => maybe.?, + else => maybe, + }; +} + +fn TypeToCreate(function: anytype) type { + const InnerType = IntermediateType(function); + return switch (@typeInfo(InnerType)) { + .Optional => |o| o.child, + else => InnerType, + }; +} + +fn IntermediateType(function: anytype) type { + const params = @typeInfo(@TypeOf(function)).Fn.params; + return @typeInfo(params[params.len - 1].type.?).Pointer.child; +} + +fn result(int: isize) LmdbError!void { + return switch (int) { + 0 => {}, + -30799 => error.MDB_KEYEXIST, + -30798 => error.MDB_NOTFOUND, + -30797 => error.MDB_PAGE_NOTFOUND, + -30796 => error.MDB_CORRUPTED, + -30795 => error.MDB_PANIC, + -30794 => error.MDB_VERSION_MISMATCH, + -30793 => error.MDB_INVALID, + -30792 => error.MDB_MAP_FULL, + -30791 => error.MDB_DBS_FULL, + -30790 => error.MDB_READERS_FULL, + -30789 => error.MDB_TLS_FULL, + -30788 => error.MDB_TXN_FULL, + -30787 => error.MDB_CURSOR_FULL, + -30786 => error.MDB_PAGE_FULL, + -30785 => error.MDB_MAP_RESIZED, + -30784 => error.MDB_INCOMPATIBLE, + -30783 => error.MDB_BAD_RSLOT, + -30782 => error.MDB_BAD_TXN, + -30781 => error.MDB_BAD_VALSIZE, + -30780 => error.MDB_BAD_DBI, + -30779 => error.MDB_PROBLEM, + 1 => error.EPERM, + 2 => error.ENOENT, + 3 => error.ESRCH, + 4 => error.EINTR, + 5 => error.EIO, + 6 => error.ENXIO, + 7 => error.E2BIG, + 8 => error.ENOEXEC, + 9 => error.EBADF, + 10 => error.ECHILD, + 11 => error.EAGAIN, + 12 => error.ENOMEM, + 13 => error.EACCES, + 14 => error.EFAULT, + 15 => error.ENOTBLK, + 16 => error.EBUSY, + 17 => error.EEXIST, + 18 => error.EXDEV, + 19 => error.ENODEV, + 20 => error.ENOTDIR, + 21 => error.EISDIR, + 22 => error.EINVAL, + 23 => error.ENFILE, + 24 => error.EMFILE, + 25 => error.ENOTTY, + 26 => error.ETXTBSY, + 27 => error.EFBIG, + 28 => error.ENOSPC, + 29 => error.ESPIPE, + 30 => error.EROFS, + 31 => error.EMLINK, + 32 => error.EPIPE, + 33 => error.EDOM, + 34 => error.ERANGE, + else => error.UnspecifiedErrorCode, + }; +} + +pub const LmdbError = error{ + //////////////////////////////////////////////////////// + /// lmdb-specific errors + //// + + /// Successful result + MDB_SUCCESS, + /// key/data pair already exists + MDB_KEYEXIST, + /// key/data pair not found (EOF) + MDB_NOTFOUND, + /// Requested page not found - this usually indicates corruption + MDB_PAGE_NOTFOUND, + /// Located page was wrong type + MDB_CORRUPTED, + /// Update of meta page failed or environment had fatal error + MDB_PANIC, + /// Environment version mismatch + MDB_VERSION_MISMATCH, + /// File is not a valid LMDB file + MDB_INVALID, + /// Environment mapsize reached + MDB_MAP_FULL, + /// Environment maxdbs reached + MDB_DBS_FULL, + /// Environment maxreaders reached + MDB_READERS_FULL, + /// Too many TLS keys in use - Windows only + MDB_TLS_FULL, + /// Txn has too many dirty pages + MDB_TXN_FULL, + /// Cursor stack too deep - internal error + MDB_CURSOR_FULL, + /// Page has not enough space - internal error + MDB_PAGE_FULL, + /// Database contents grew beyond environment mapsize + MDB_MAP_RESIZED, + /// Operation and DB incompatible, or DB type changed. This can mean: + /// The operation expects an #MDB_DUPSORT / #MDB_DUPFIXED database. + /// Opening a named DB when the unnamed DB has #MDB_DUPSORT / #MDB_INTEGERKEY. + /// Accessing a data record as a database, or vice versa. + /// The database was dropped and recreated with different flags. + MDB_INCOMPATIBLE, + /// Invalid reuse of reader locktable slot + MDB_BAD_RSLOT, + /// Transaction must abort, has a child, or is invalid + MDB_BAD_TXN, + /// Unsupported size of key/DB name/data, or wrong DUPFIXED size + MDB_BAD_VALSIZE, + /// The specified DBI was changed unexpectedly + MDB_BAD_DBI, + /// Unexpected problem - txn should abort + MDB_PROBLEM, + + //////////////////////////////////////////////////////// + /// asm-generic errors - may be thrown by lmdb + //// + + /// Operation not permitted + EPERM, + /// No such file or directory + ENOENT, + /// No such process + ESRCH, + /// Interrupted system call + EINTR, + /// I/O error + EIO, + /// No such device or address + ENXIO, + /// Argument list too long + E2BIG, + /// Exec format error + ENOEXEC, + /// Bad file number + EBADF, + /// No child processes + ECHILD, + /// Try again + EAGAIN, + /// Out of memory + ENOMEM, + /// Permission denied + EACCES, + /// Bad address + EFAULT, + /// Block device required + ENOTBLK, + /// Device or resource busy + EBUSY, + /// File exists + EEXIST, + /// Cross-device link + EXDEV, + /// No such device + ENODEV, + /// Not a directory + ENOTDIR, + /// Is a directory + EISDIR, + /// Invalid argument + EINVAL, + /// File table overflow + ENFILE, + /// Too many open files + EMFILE, + /// Not a typewriter + ENOTTY, + /// Text file busy + ETXTBSY, + /// File too large + EFBIG, + /// No space left on device + ENOSPC, + /// Illegal seek + ESPIPE, + /// Read-only file system + EROFS, + /// Too many links + EMLINK, + /// Broken pipe + EPIPE, + /// Math argument out of domain of func + EDOM, + /// Math result not representable + ERANGE, + + //////////////////////////////////////////////////////// + /// errors interfacing with Lmdb + //// + + /// Got a return value that is not specified in LMDB's header files + UnspecifiedErrorCode, +}; + +test "lmdb database" { + try database.interface.testDatabase(LMDB); +} diff --git a/src/utils/allocators.zig b/src/utils/allocators.zig index 7f2dc28b1..973d25321 100644 --- a/src/utils/allocators.zig +++ b/src/utils/allocators.zig @@ -445,6 +445,10 @@ pub const failing = struct { } }; +pub fn noAlloc(_: *anyopaque, _: usize, _: u8, _: usize) ?[*]u8 { + return null; +} + test "recycle allocator: tryCollapse" { const bytes_allocator = std.testing.allocator; var allocator = try RecycleFBA(.{}).init(.{ From 427fce79c5acb3fe0bcedfcfe6061be47c11a6f3 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 29 Oct 2024 20:57:12 -0400 Subject: [PATCH 02/32] feat(ledger): wip lmdb --- src/ledger/database/lmdb.zig | 131 +++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 23 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 2ca89a486..08e2d4ae3 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -155,11 +155,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { ) anyerror!void { const start_bytes = try key_serializer.serializeToRef(self.allocator, start); defer start_bytes.deinit(); - const end_bytes = try key_serializer.serializeToRef(self.allocator, end); defer end_bytes.deinit(); - @panic("TODO"); // TODO + var batch = try self.initWriteBatch(); + errdefer batch.deinit(); + try batch.deleteRange(start, end); + try self.commit(batch); } pub fn initWriteBatch(self: *Self) LmdbError!WriteBatch { @@ -186,11 +188,11 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { /// - this name avoids confusion with solana transactions pub const WriteBatch = struct { allocator: Allocator, - inner: *c.MDB_txn, + txn: *c.MDB_txn, cf_handles: []const c.MDB_dbi, pub fn deinit(self: *WriteBatch) void { - self.inner.deinit(); + c.mdb_txn_reset(self.txn); } pub fn put( @@ -216,7 +218,9 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { ) anyerror!void { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - self.inner.delete(self.cf_handles[cf.find(column_families)], key_bytes.data); + + const key_val = toVal(key_bytes.data); + try result(c.mdb_del(self.txn, cf.find(column_families), &key_val, 0)); } pub fn deleteRange( @@ -227,15 +231,25 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { ) anyerror!void { const start_bytes = try key_serializer.serializeToRef(self.allocator, start); defer start_bytes.deinit(); - const end_bytes = try key_serializer.serializeToRef(self.allocator, end); defer end_bytes.deinit(); - self.inner.deleteRange( - self.cf_handles[cf.find(column_families)], - start_bytes.data, - end_bytes.data, - ); + const cursor = try ret(c.mdb_cursor_open(self.txn, cf.find(column_families))); + defer result(c.mdb_cursor_close(cursor)); + + var key_val = toVal(start_bytes); + var val_val: c.MDB_val = undefined; + try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); + + while (std.mem.lessThan(u8, fromVal(key_val), end_bytes.data)) { + try result(c.mdb_cursor_del(cursor, 0)); + try result(c.mdb_cursor_get( + cursor, + &key_val, + &val_val, + cursorOp(.GET_CURRENT), + )); + } } }; @@ -245,27 +259,46 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { comptime direction: IteratorDirection, start: ?cf.Key, ) anyerror!Iterator(cf, direction) { - const start_bytes = if (start) |s| try key_serializer.serializeToRef(self.allocator, s) else null; - defer if (start_bytes) |sb| sb.deinit(); + const maybe_start_bytes = if (start) |s| + try key_serializer.serializeToRef(self.allocator, s) + else + null; + defer if (maybe_start_bytes) |sb| sb.deinit(); + + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + errdefer c.mdb_txn_reset(txn); + + const cursor = try ret(c.mdb_cursor_open(self.txn, cf.find(column_families))); + errdefer result(c.mdb_cursor_close(cursor)); + + var key_val: c.MDB_val = undefined; + var val_val: c.MDB_val = undefined; + if (maybe_start_bytes) |start_bytes| { + key_val = toVal(start_bytes); + try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); + } else { + const operation = switch (direction) { + .forward => cursorOp(.FIRST), + .reverse => cursorOp(.LAST), + }; + try result(c.mdb_cursor_get(cursor, &key_val, &val_val, operation)); + } + return .{ .allocator = self.allocator, .logger = self.logger, - .inner = self.db.iterator( - self.cf_handles[cf.find(column_families)], - switch (direction) { - .forward => .forward, - .reverse => .reverse, - }, - if (start_bytes) |s| s.data else null, - ), + .txn = txn, + .cursot = cursor, + .direction = direction, }; } pub fn Iterator(cf: ColumnFamily, _: IteratorDirection) type { return struct { allocator: Allocator, - inner: rocks.Iterator, - logger: Logger, + txn: c.MDB_txn, + cursor: c.MDB_cursor, + direction: IteratorDirection, /// Calling this will free all slices returned by the iterator pub fn deinit(self: *@This()) void { @@ -366,6 +399,58 @@ fn IntermediateType(function: anytype) type { return @typeInfo(params[params.len - 1].type.?).Pointer.child; } +fn cursorOp(operation: CursorOperation) c_int { + return @intFromEnum(operation); +} + +/// Cursor Get operations. +/// +/// This is the set of all operations for retrieving data +/// using a cursor. +const CursorOperation = enum(c_int) { + /// Position at first key/data item + FIRST, + /// Position at first data item of current key. Only for #MDB_DUPSORT + FIRST_DUP, + /// Position at key/data pair. Only for #MDB_DUPSORT + GET_BOTH, + /// position at key, nearest data. Only for #MDB_DUPSORT + GET_BOTH_RANGE, + /// Return key/data at current cursor position + GET_CURRENT, + /// Return up to a page of duplicate data items from current cursor position. + /// Move cursor to prepare for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED + GET_MULTIPLE, + /// Position at last key/data item + LAST, + /// Position at last data item of current key. Only for #MDB_DUPSORT + LAST_DUP, + /// Position at next data item + NEXT, + /// Position at next data item of current key. Only for #MDB_DUPSORT + NEXT_DUP, + /// Return up to a page of duplicate data items from next cursor position. + /// Move cursor to prepare for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED + NEXT_MULTIPLE, + /// Position at first data item of next key + NEXT_NODUP, + /// Position at previous data item + PREV, + /// Position at previous data item of current key. Only for #MDB_DUPSORT + PREV_DUP, + /// Position at last data item of previous key + PREV_NODUP, + /// Position at specified key + SET, + /// Position at specified key, return key + data + SET_KEY, + /// Position at first key greater than or equal to specified key. + SET_RANGE, + /// Position at previous page and return up to a page of duplicate data items. + /// Only for #MDB_DUPFIXED + PREV_MULTIPLE, +}; + fn result(int: isize) LmdbError!void { return switch (int) { 0 => {}, From f1fd0cf2ca13d021c2108144408d551d65fee6ed Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 15:28:32 -0400 Subject: [PATCH 03/32] feat(ledger): write last lmdb methods, but still doesn't compile --- src/ledger/database/lmdb.zig | 99 +++++++++++++++++++++--------------- src/ledger/result_writer.zig | 2 +- src/ledger/tests.zig | 2 +- src/tests.zig | 1 + 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 08e2d4ae3..1ebfa1ecf 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -23,7 +23,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const Self = @This(); - pub fn open(allocator: Allocator, logger: Logger, path: []const u8) LmdbError!Self { + pub fn open(allocator: Allocator, logger: Logger, path: []const u8) anyerror!Self { const owned_path = try allocator.dupe(u8, path); // create and open the database @@ -91,8 +91,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); - const key_val = toVal(key_bytes.data); - const val_val = toVal(val_bytes.data); + var key_val = toVal(key_bytes.data); + var val_val = toVal(val_bytes.data); try result(c.mdb_put(txn, cf.find(column_families), &key_val, &val_val, 0)); try result(c.mdb_txn_commit, .{txn}); } @@ -105,7 +105,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { ) anyerror!?cf.Value { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - const key_val = toVal(key_bytes); + var key_val = toVal(key_bytes); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); defer c.mdb_txn_reset(txn); @@ -118,7 +118,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn getBytes(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!?BytesRef { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - const key_val = toVal(key_bytes); + var key_val = toVal(key_bytes); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); @@ -138,7 +138,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn delete(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!void { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - const key_val = toVal(key_bytes); + var key_val = toVal(key_bytes); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); @@ -167,7 +167,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn initWriteBatch(self: *Self) LmdbError!WriteBatch { return .{ .allocator = self.allocator, - .inner = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }), + .txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }), .cf_handles = self.cf_handles, }; } @@ -206,8 +206,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const val_bytes = try value_serializer.serializeToRef(self.allocator, value); defer val_bytes.deinit(); - const key_val = toVal(key_bytes.data); - const val_val = toVal(val_bytes.data); + var key_val = toVal(key_bytes.data); + var val_val = toVal(val_bytes.data); try result(c.mdb_put(self.txn, cf.find(column_families), &key_val, &val_val, 0)); } @@ -219,7 +219,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - const key_val = toVal(key_bytes.data); + var key_val = toVal(key_bytes.data); try result(c.mdb_del(self.txn, cf.find(column_families), &key_val, 0)); } @@ -278,10 +278,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); } else { const operation = switch (direction) { - .forward => cursorOp(.FIRST), - .reverse => cursorOp(.LAST), + .forward => .FIRST, + .reverse => .LAST, }; - try result(c.mdb_cursor_get(cursor, &key_val, &val_val, operation)); + try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(operation))); } return .{ @@ -289,55 +289,70 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { .logger = self.logger, .txn = txn, .cursot = cursor, - .direction = direction, + .direction = switch (direction) { + .forward => .NEXT, + .reverse => .PREV, + }, }; } pub fn Iterator(cf: ColumnFamily, _: IteratorDirection) type { return struct { allocator: Allocator, - txn: c.MDB_txn, - cursor: c.MDB_cursor, - direction: IteratorDirection, + txn: *c.MDB_txn, + cursor: *c.MDB_cursor, + direction: CursorOperation, + next_operation: CursorOperation = .GET_CURRENT, /// Calling this will free all slices returned by the iterator pub fn deinit(self: *@This()) void { - self.inner.deinit(); + c.mdb_cursor_close(self.cursor); + c.mdb_txn_abort(self.txn); } pub fn next(self: *@This()) anyerror!?cf.Entry() { - const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); - return if (entry) |kv| { - return .{ - try key_serializer.deserialize(cf.Key, self.allocator, kv[0].data), - try value_serializer.deserialize(cf.Value, self.allocator, kv[1].data), - }; - } else null; + const key, const val = try self.nextImpl() orelse return null; + return .{ + try key_serializer.deserialize(cf.Key, self.allocator, key), + try value_serializer.deserialize(cf.Value, self.allocator, val), + }; } pub fn nextKey(self: *@This()) anyerror!?cf.Key { - const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); - return if (entry) |kv| - try key_serializer.deserialize(cf.Key, self.allocator, kv[0].data) - else - null; + const key, _ = try self.nextImpl() orelse return null; + return try key_serializer.deserialize(cf.Key, self.allocator, key); } pub fn nextValue(self: *@This()) anyerror!?cf.Value { - const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); - return if (entry) |kv| - try key_serializer.deserialize(cf.Value, self.allocator, kv[1].data) - else - null; + _, const val = try self.nextImpl() orelse return null; + return try key_serializer.deserialize(cf.Key, self.allocator, val); } /// Returned data does not outlive the iterator. pub fn nextBytes(self: *@This()) LmdbError!?[2]BytesRef { - const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); - return if (entry) |kv| .{ - .{ .allocator = null, .data = kv[0].data }, - .{ .allocator = null, .data = kv[1].data }, - } else null; + const key, const val = try self.nextImpl() orelse return null; + return .{ + .{ .allocator = null, .data = key }, + .{ .allocator = null, .data = val }, + }; + } + + fn nextImpl(self: *@This()) LmdbError!?struct { []const u8, []const u8 } { + var key_val: c.MDB_val = undefined; + var val_val: c.MDB_val = undefined; + result(c.mdb_cursor_get( + self.cursor, + &key_val, + &val_val, + cursorOp(self.next_operation), + )) catch |e| switch (e) { + error.MDB_NOTFOUND => return null, + else => return e, + }; + + self.next_operation = self.direction; + + return .{ fromVal(key_val), fromVal(val_val) }; } }; } @@ -399,7 +414,7 @@ fn IntermediateType(function: anytype) type { return @typeInfo(params[params.len - 1].type.?).Pointer.child; } -fn cursorOp(operation: CursorOperation) c_int { +fn cursorOp(operation: CursorOperation) c_uint { return @intFromEnum(operation); } @@ -407,7 +422,7 @@ fn cursorOp(operation: CursorOperation) c_int { /// /// This is the set of all operations for retrieving data /// using a cursor. -const CursorOperation = enum(c_int) { +const CursorOperation = enum(c_uint) { /// Position at first key/data item FIRST, /// Position at first data item of current key. Only for #MDB_DUPSORT diff --git a/src/ledger/result_writer.zig b/src/ledger/result_writer.zig index 14264f6b2..850c5a02a 100644 --- a/src/ledger/result_writer.zig +++ b/src/ledger/result_writer.zig @@ -111,7 +111,7 @@ pub const LedgerResultWriter = struct { self: *Self, duplicate_confirmed_slot_hashes: []struct { Slot, Hash }, ) !void { - var write_batch = try self.db.writeBatch(); + var write_batch = try self.db.initWriteBatch(); for (duplicate_confirmed_slot_hashes) |slot_hash| { const slot, const frozen_hash = slot_hash; const data = FrozenHashVersioned{ .current = FrozenHashStatus{ diff --git a/src/ledger/tests.zig b/src/ledger/tests.zig index af77e32c0..e971cffe7 100644 --- a/src/ledger/tests.zig +++ b/src/ledger/tests.zig @@ -204,7 +204,7 @@ pub fn loadShredsFromFile(allocator: Allocator, path: []const u8) ![]const Shred pub fn saveShredsToFile(path: []const u8, shreds: []const Shred) !void { const file = try std.fs.cwd().createFile(path, .{}); - for (shreds) |s| writeChunk(file.writer(), s.payload()); + for (shreds) |s| try writeChunk(file.writer(), s.payload()); } fn readChunk(allocator: Allocator, reader: anytype) !?[]const u8 { diff --git a/src/tests.zig b/src/tests.zig index bd5f7565b..8efb0228e 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -6,6 +6,7 @@ const sig = @import("sig.zig"); test { std.testing.log_level = std.log.Level.err; refAllDeclsRecursive(sig, 2); + refAllDeclsRecursive(sig.ledger, 2); } /// Like std.testing.refAllDeclsRecursive, except: From 3fc08c7f07cde1796fa7fc6ef2b7c1cb4d5fc0dd Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 15:50:29 -0400 Subject: [PATCH 04/32] feat(ledger): lmdb compiles. tests fail --- src/ledger/database/lmdb.zig | 58 +++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 1ebfa1ecf..2011da61f 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -23,13 +23,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const Self = @This(); - pub fn open(allocator: Allocator, logger: Logger, path: []const u8) anyerror!Self { + pub fn open(allocator: Allocator, _: Logger, path: []const u8) anyerror!Self { const owned_path = try allocator.dupe(u8, path); // create and open the database const env = try ret(c.mdb_env_create, .{}); try result(c.mdb_env_set_maxdbs(env, column_families.len)); - try result(c.mdb_env_open(env, path, 0, 0o700)); + try result(c.mdb_env_open(env, @ptrCast(path), 0, 0o700)); // begin transaction to create column families aka "databases" in lmdb const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); @@ -40,18 +40,21 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { errdefer allocator.free(cf_handles); // save cf handles - for (column_families, 0..) |cf, i| { + inline for (column_families, 0..) |cf, i| { // open cf/database, creating if necessary - cf_handles[i] = try ret(c.mdb_dbi_open, .{ txn, cf.name, 0x40000 }); + cf_handles[i] = try ret(c.mdb_dbi_open, .{ + txn, + @as([*c]const u8, @ptrCast(cf.name)), + 0x40000, // create if missing + }); } // persist column families - try result(c.mdb_txn_commit, .{txn}); + try result(c.mdb_txn_commit(txn)); return .{ .allocator = allocator, - .db = env, - .logger = logger, + .env = env, .cf_handles = cf_handles, .path = owned_path, }; @@ -59,7 +62,6 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn deinit(self: *Self) void { self.allocator.free(self.cf_handles); - self.db.deinit(); self.allocator.free(self.path); } @@ -94,7 +96,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); try result(c.mdb_put(txn, cf.find(column_families), &key_val, &val_val, 0)); - try result(c.mdb_txn_commit, .{txn}); + try result(c.mdb_txn_commit(txn)); } pub fn get( @@ -105,12 +107,12 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { ) anyerror!?cf.Value { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - var key_val = toVal(key_bytes); + var key_val = toVal(key_bytes.data); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); defer c.mdb_txn_reset(txn); - const value = try ret(c.mdb_get(txn, cf.find(column_families), &key_val)); + const value = try ret(c.mdb_get, .{ txn, cf.find(column_families), &key_val }); return try value_serializer.deserialize(cf.Value, allocator, fromVal(value)); } @@ -118,12 +120,12 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn getBytes(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!?BytesRef { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - var key_val = toVal(key_bytes); + var key_val = toVal(key_bytes.data); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); - const item = try ret(c.mdb_get(txn, cf.find(column_families), &key_val)); + const item = try ret(c.mdb_get, .{ txn, cf.find(column_families), &key_val }); return .{ .allocator = txnResetter(txn), @@ -138,12 +140,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn delete(self: *Self, comptime cf: ColumnFamily, key: cf.Key) anyerror!void { const key_bytes = try key_serializer.serializeToRef(self.allocator, key); defer key_bytes.deinit(); - var key_val = toVal(key_bytes); + var key_val = toVal(key_bytes.data); + var val_val: c.MDB_val = undefined; const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); - try result(c.mdb_del(txn, cf.find(column_families), &key_val)); + try result(c.mdb_del(txn, cf.find(column_families), &key_val, &val_val)); try result(c.mdb_txn_commit(txn)); } @@ -173,7 +176,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } pub fn commit(_: *Self, batch: WriteBatch) LmdbError!void { - try result(c.mdb_txn_commit(batch.inner)); + try result(c.mdb_txn_commit(batch.txn)); } /// A write batch is a sequence of operations that execute atomically. @@ -234,8 +237,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const end_bytes = try key_serializer.serializeToRef(self.allocator, end); defer end_bytes.deinit(); - const cursor = try ret(c.mdb_cursor_open(self.txn, cf.find(column_families))); - defer result(c.mdb_cursor_close(cursor)); + const cursor = try ret(c.mdb_cursor_open, .{ self.txn, cf.find(column_families) }); + defer c.mdb_cursor_close(cursor); var key_val = toVal(start_bytes); var val_val: c.MDB_val = undefined; @@ -268,13 +271,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_reset(txn); - const cursor = try ret(c.mdb_cursor_open(self.txn, cf.find(column_families))); - errdefer result(c.mdb_cursor_close(cursor)); + const cursor = try ret(c.mdb_cursor_open, .{ txn, cf.find(column_families) }); + errdefer c.mdb_cursor_close(cursor); var key_val: c.MDB_val = undefined; var val_val: c.MDB_val = undefined; if (maybe_start_bytes) |start_bytes| { - key_val = toVal(start_bytes); + key_val = toVal(start_bytes.data); try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); } else { const operation = switch (direction) { @@ -286,9 +289,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { return .{ .allocator = self.allocator, - .logger = self.logger, .txn = txn, - .cursot = cursor, + .cursor = cursor, .direction = switch (direction) { .forward => .NEXT, .reverse => .PREV, @@ -342,8 +344,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var val_val: c.MDB_val = undefined; result(c.mdb_cursor_get( self.cursor, - &key_val, - &val_val, + @ptrCast(&key_val), + @ptrCast(&val_val), cursorOp(self.next_operation), )) catch |e| switch (e) { error.MDB_NOTFOUND => return null, @@ -366,7 +368,7 @@ fn toVal(bytes: []const u8) c.MDB_val { }; } -fn fromVal(value: [*c]c.MDB_val) []const u8 { +fn fromVal(value: c.MDB_val) []const u8 { const ptr: [*c]u8 = @ptrCast(value.mv_data); return ptr[0..value.mv_size]; } @@ -663,6 +665,6 @@ pub const LmdbError = error{ UnspecifiedErrorCode, }; -test "lmdb database" { - try database.interface.testDatabase(LMDB); +comptime { + _ = &database.interface.testDatabase(LMDB); } From 3f0fe869c5269a745b9fe555105101f9634a70de Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 18:58:39 -0400 Subject: [PATCH 05/32] feat(ledger): get all the tests working for lmdb and add some more --- src/ledger/database/hashmap.zig | 16 +- src/ledger/database/interface.zig | 164 +++++++++++++++++++ src/ledger/database/lmdb.zig | 258 ++++++++++++++++++------------ 3 files changed, 329 insertions(+), 109 deletions(-) diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index f7be1ff5b..edd12011f 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -178,9 +178,17 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { const cf_index, const key = delete_ix; self.maps[cf_index].delete(self.allocator, key); }, - .delete_range => { - // TODO: also add to database tests - @panic("not implemented"); + .delete_range => |delete_range_ix| { + const cf_index, const start, const end = delete_range_ix; + const keys, _ = self.maps[cf_index].map.range(start, end); + const to_delete = try batch.allocator.alloc([]const u8, keys.len); + defer batch.allocator.free(to_delete); + for (keys, 0..) |key, i| { + to_delete[i] = key; + } + for (to_delete) |delete_key| { + self.maps[cf_index].delete(self.allocator, delete_key); + } }, } } @@ -266,7 +274,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { const end_bytes = try key_serializer.serializeAlloc(self.allocator, end); errdefer self.allocator.free(end_bytes); const cf_index = cf.find(column_families); - self.instructions.append( + try self.instructions.append( self.allocator, .{ .delete_range = .{ cf_index, start_bytes, end_bytes } }, ); diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index 5840e3c17..b05283406 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -536,5 +536,169 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } + + pub fn @"iterator forward start before all"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + try db.put(cf1, 40, .{ .hello = 44 }); + try db.put(cf1, 10, .{ .hello = 111 }); + try db.put(cf1, 30, .{ .hello = 33 }); + try db.put(cf1, 20, .{ .hello = 222 }); + + var iter = try db.iterator(cf1, .forward, 5); + defer iter.deinit(); + + var next = (try iter.next()).?; + try std.testing.expectEqual(10, next[0]); + try std.testing.expectEqual(Value1{ .hello = 111 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(20, next[0]); + try std.testing.expectEqual(Value1{ .hello = 222 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(30, next[0]); + try std.testing.expectEqual(Value1{ .hello = 33 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(40, next[0]); + try std.testing.expectEqual(Value1{ .hello = 44 }, next[1]); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator forward start after all"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + try db.put(cf1, 40, .{ .hello = 44 }); + try db.put(cf1, 10, .{ .hello = 111 }); + try db.put(cf1, 30, .{ .hello = 33 }); + try db.put(cf1, 20, .{ .hello = 222 }); + + var iter = try db.iterator(cf1, .forward, 50); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator reverse start before all"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + try db.put(cf1, 40, .{ .hello = 44 }); + try db.put(cf1, 10, .{ .hello = 111 }); + try db.put(cf1, 30, .{ .hello = 33 }); + try db.put(cf1, 20, .{ .hello = 222 }); + + var iter = try db.iterator(cf1, .reverse, 50); + defer iter.deinit(); + + var next = (try iter.next()).?; + try std.testing.expectEqual(40, next[0]); + try std.testing.expectEqual(Value1{ .hello = 44 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(30, next[0]); + try std.testing.expectEqual(Value1{ .hello = 33 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(20, next[0]); + try std.testing.expectEqual(Value1{ .hello = 222 }, next[1]); + next = (try iter.next()).?; + try std.testing.expectEqual(10, next[0]); + try std.testing.expectEqual(Value1{ .hello = 111 }, next[1]); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator reverse start after all"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + try db.put(cf1, 40, .{ .hello = 44 }); + try db.put(cf1, 10, .{ .hello = 111 }); + try db.put(cf1, 30, .{ .hello = 33 }); + try db.put(cf1, 20, .{ .hello = 222 }); + + var iter = try db.iterator(cf1, .reverse, 5); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator forward empty"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + var iter = try db.iterator(cf1, .forward, 1); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator reverse empty"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + var iter = try db.iterator(cf1, .reverse, 1); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator forward empty with null start"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + var iter = try db.iterator(cf1, .forward, null); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"iterator reverse empty with null start"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + var iter = try db.iterator(cf1, .reverse, null); + defer iter.deinit(); + + try std.testing.expectEqual(null, try iter.next()); + } + + pub fn @"WriteBatch.deleteRange"() !void { + const path = test_dir ++ @src().fn_name; + try ledger.tests.freshDir(path); + var db = try DB.open(allocator, logger, path); + defer db.deinit(); + + try db.put(cf1, 40, .{ .hello = 44 }); + try db.put(cf1, 10, .{ .hello = 111 }); + try db.put(cf1, 30, .{ .hello = 33 }); + try db.put(cf1, 20, .{ .hello = 222 }); + + var batch = try db.initWriteBatch(); + defer batch.deinit(); + try batch.deleteRange(cf1, 0, 100); + try db.commit(batch); + + try std.testing.expectEqual(null, try db.get(allocator, cf1, 10)); + try std.testing.expectEqual(null, try db.get(allocator, cf1, 20)); + try std.testing.expectEqual(null, try db.get(allocator, cf1, 30)); + try std.testing.expectEqual(null, try db.get(allocator, cf1, 40)); + } }; } diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 2011da61f..ff1bde1d9 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -18,7 +18,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { return struct { allocator: Allocator, env: *c.MDB_env, - cf_handles: []const c.MDB_dbi, + dbis: []const c.MDB_dbi, path: []const u8, const Self = @This(); @@ -33,19 +33,19 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // begin transaction to create column families aka "databases" in lmdb const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); - errdefer c.mdb_txn_reset(txn); + errdefer c.mdb_txn_abort(txn); // allocate cf handles - const cf_handles = try allocator.alloc(c.MDB_dbi, column_families.len); - errdefer allocator.free(cf_handles); + const dbis = try allocator.alloc(c.MDB_dbi, column_families.len); + errdefer allocator.free(dbis); // save cf handles inline for (column_families, 0..) |cf, i| { // open cf/database, creating if necessary - cf_handles[i] = try ret(c.mdb_dbi_open, .{ + dbis[i] = try ret(c.mdb_dbi_open, .{ txn, @as([*c]const u8, @ptrCast(cf.name)), - 0x40000, // create if missing + MDB_CREATE, }); } @@ -55,16 +55,20 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { return .{ .allocator = allocator, .env = env, - .cf_handles = cf_handles, + .dbis = dbis, .path = owned_path, }; } pub fn deinit(self: *Self) void { - self.allocator.free(self.cf_handles); + self.allocator.free(self.dbis); self.allocator.free(self.path); } + fn dbi(self: *Self, comptime cf: ColumnFamily) c.MDB_dbi { + return self.dbis[cf.find(column_families)]; + } + pub fn count(self: *Self, comptime cf: ColumnFamily) Allocator.Error!u64 { const live_files = try self.db.liveFiles(self.allocator); defer live_files.deinit(); @@ -91,11 +95,11 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer val_bytes.deinit(); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_reset(txn); + errdefer c.mdb_txn_abort(txn); var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); - try result(c.mdb_put(txn, cf.find(column_families), &key_val, &val_val, 0)); + try result(c.mdb_put(txn, self.dbi(cf), &key_val, &val_val, 0)); try result(c.mdb_txn_commit(txn)); } @@ -109,10 +113,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); - defer c.mdb_txn_reset(txn); + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + defer c.mdb_txn_abort(txn); - const value = try ret(c.mdb_get, .{ txn, cf.find(column_families), &key_val }); + const value = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { + error.MDB_NOTFOUND => return null, + else => return e, + }; return try value_serializer.deserialize(cf.Value, allocator, fromVal(value)); } @@ -122,10 +129,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_reset(txn); + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + errdefer c.mdb_txn_abort(txn); - const item = try ret(c.mdb_get, .{ txn, cf.find(column_families), &key_val }); + const item = try ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { + error.MDB_NOTFOUND => return null, + else => return e, + }; return .{ .allocator = txnResetter(txn), @@ -144,9 +154,12 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var val_val: c.MDB_val = undefined; const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_reset(txn); + errdefer c.mdb_txn_abort(txn); - try result(c.mdb_del(txn, cf.find(column_families), &key_val, &val_val)); + result(c.mdb_del(txn, self.dbi(cf), &key_val, &val_val)) catch |e| switch (e) { + error.MDB_NOTFOUND => {}, + else => return e, + }; try result(c.mdb_txn_commit(txn)); } @@ -156,22 +169,21 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { start: cf.Key, end: cf.Key, ) anyerror!void { - const start_bytes = try key_serializer.serializeToRef(self.allocator, start); - defer start_bytes.deinit(); - const end_bytes = try key_serializer.serializeToRef(self.allocator, end); - defer end_bytes.deinit(); - var batch = try self.initWriteBatch(); errdefer batch.deinit(); - try batch.deleteRange(start, end); + try batch.deleteRange(cf, start, end); try self.commit(batch); } - pub fn initWriteBatch(self: *Self) LmdbError!WriteBatch { + pub fn initWriteBatch(self: *Self) anyerror!WriteBatch { + const executed = try self.allocator.create(bool); + errdefer self.allocator.destroy(executed); + executed.* = false; return .{ .allocator = self.allocator, .txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }), - .cf_handles = self.cf_handles, + .dbis = self.dbis, + .executed = executed, }; } @@ -192,10 +204,16 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub const WriteBatch = struct { allocator: Allocator, txn: *c.MDB_txn, - cf_handles: []const c.MDB_dbi, + dbis: []const c.MDB_dbi, + executed: *bool, pub fn deinit(self: *WriteBatch) void { - c.mdb_txn_reset(self.txn); + if (!self.executed.*) c.mdb_txn_abort(self.txn); + self.allocator.destroy(self.executed); + } + + fn dbi(self: *WriteBatch, comptime cf: ColumnFamily) c.MDB_dbi { + return self.dbis[cf.find(column_families)]; } pub fn put( @@ -211,7 +229,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); - try result(c.mdb_put(self.txn, cf.find(column_families), &key_val, &val_val, 0)); + try result(c.mdb_put(self.txn, self.dbi(cf), &key_val, &val_val, 0)); } pub fn delete( @@ -223,7 +241,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - try result(c.mdb_del(self.txn, cf.find(column_families), &key_val, 0)); + try result(c.mdb_del(self.txn, self.dbi(cf), &key_val, 0)); } pub fn deleteRange( @@ -237,21 +255,17 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const end_bytes = try key_serializer.serializeToRef(self.allocator, end); defer end_bytes.deinit(); - const cursor = try ret(c.mdb_cursor_open, .{ self.txn, cf.find(column_families) }); + const cursor = try ret(c.mdb_cursor_open, .{ self.txn, self.dbi(cf) }); defer c.mdb_cursor_close(cursor); - var key_val = toVal(start_bytes); - var val_val: c.MDB_val = undefined; - try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); + var key, _ = if (try cursorGet(cursor, start_bytes.data, .set_range)) |kv| + kv + else + return; - while (std.mem.lessThan(u8, fromVal(key_val), end_bytes.data)) { + while (std.mem.lessThan(u8, key, end_bytes.data)) { try result(c.mdb_cursor_del(cursor, 0)); - try result(c.mdb_cursor_get( - cursor, - &key_val, - &val_val, - cursorOp(.GET_CURRENT), - )); + key, _ = try cursorGetRelative(cursor, .next) orelse return; } } }; @@ -269,22 +283,28 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer if (maybe_start_bytes) |sb| sb.deinit(); const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_reset(txn); + errdefer c.mdb_txn_abort(txn); - const cursor = try ret(c.mdb_cursor_open, .{ txn, cf.find(column_families) }); + const cursor = try ret(c.mdb_cursor_open, .{ txn, self.dbi(cf) }); errdefer c.mdb_cursor_close(cursor); - var key_val: c.MDB_val = undefined; - var val_val: c.MDB_val = undefined; - if (maybe_start_bytes) |start_bytes| { - key_val = toVal(start_bytes.data); - try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(.SET))); - } else { - const operation = switch (direction) { - .forward => .FIRST, - .reverse => .LAST, - }; - try result(c.mdb_cursor_get(cursor, &key_val, &val_val, cursorOp(operation))); + var start_operation: CursorRelativeOperation = .get_current; + + if (null == try cursorGetRelative(cursor, .first)) { + // if the db is empty, it has this annoying behavior where a call to + // GET_CURRENT results in EINVAL. but we want it to return NOTFOUND. + // calling NEXT ensures that NOTFOUND will be returned. + start_operation = .next; + } else if (maybe_start_bytes) |start_bytes| { + switch (direction) { + .forward => _ = try cursorGet(cursor, start_bytes.data, .set_range), + .reverse => if (null == try cursorGet(cursor, start_bytes.data, .set)) { + _ = try cursorGet(cursor, start_bytes.data, .set_range); + start_operation = .prev; + }, + } + } else if (direction == .reverse) { + _ = try cursorGetRelative(cursor, .last); } return .{ @@ -292,9 +312,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { .txn = txn, .cursor = cursor, .direction = switch (direction) { - .forward => .NEXT, - .reverse => .PREV, + .forward => .next, + .reverse => .prev, }, + .next_operation = start_operation, }; } @@ -303,8 +324,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { allocator: Allocator, txn: *c.MDB_txn, cursor: *c.MDB_cursor, - direction: CursorOperation, - next_operation: CursorOperation = .GET_CURRENT, + direction: CursorRelativeOperation, + next_operation: CursorRelativeOperation, /// Calling this will free all slices returned by the iterator pub fn deinit(self: *@This()) void { @@ -340,21 +361,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } fn nextImpl(self: *@This()) LmdbError!?struct { []const u8, []const u8 } { - var key_val: c.MDB_val = undefined; - var val_val: c.MDB_val = undefined; - result(c.mdb_cursor_get( - self.cursor, - @ptrCast(&key_val), - @ptrCast(&val_val), - cursorOp(self.next_operation), - )) catch |e| switch (e) { - error.MDB_NOTFOUND => return null, - else => return e, - }; - - self.next_operation = self.direction; - - return .{ fromVal(key_val), fromVal(val_val) }; + defer self.next_operation = self.direction; + return try cursorGetRelative(self.cursor, self.next_operation); } }; } @@ -386,7 +394,7 @@ fn txnResetter(txn: *c.MDB_txn) Allocator { fn resetTxnFree(ctx: *anyopaque, _: []u8, _: u8, _: usize) void { const txn: *c.MDB_txn = @ptrCast(@alignCast(ctx)); - c.mdb_txn_reset(txn); + c.mdb_txn_abort(txn); } fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) { @@ -416,56 +424,96 @@ fn IntermediateType(function: anytype) type { return @typeInfo(params[params.len - 1].type.?).Pointer.child; } -fn cursorOp(operation: CursorOperation) c_uint { - return @intFromEnum(operation); +const MDB_CREATE = 0x40000; +const MDB_RDONLY = 0x20000; + +fn cursorGet( + cursor: *c.MDB_cursor, + key: []const u8, + operation: CursorAbsoluteOperation, +) LmdbError!?struct { []const u8, []const u8 } { + var key_val = toVal(key); + var val_val: c.MDB_val = undefined; + result(c.mdb_cursor_get( + cursor, + &key_val, + &val_val, + @intFromEnum(operation), + )) catch |err| switch (err) { + error.MDB_NOTFOUND => return null, + else => return err, + }; + return .{ fromVal(key_val), fromVal(val_val) }; } -/// Cursor Get operations. -/// -/// This is the set of all operations for retrieving data -/// using a cursor. -const CursorOperation = enum(c_uint) { - /// Position at first key/data item - FIRST, - /// Position at first data item of current key. Only for #MDB_DUPSORT - FIRST_DUP, +fn cursorGetRelative( + cursor: *c.MDB_cursor, + operation: CursorRelativeOperation, +) LmdbError!?struct { []const u8, []const u8 } { + var key_val: c.MDB_val = undefined; + var val_val: c.MDB_val = undefined; + result(c.mdb_cursor_get( + cursor, + &key_val, + &val_val, + @intFromEnum(operation), + )) catch |err| switch (err) { + error.MDB_NOTFOUND => return null, + else => return err, + }; + return .{ fromVal(key_val), fromVal(val_val) }; +} + +/// Cursor Get operations that require a key to execute a lookup +const CursorAbsoluteOperation = enum(c_uint) { /// Position at key/data pair. Only for #MDB_DUPSORT - GET_BOTH, + get_both = 2, /// position at key, nearest data. Only for #MDB_DUPSORT - GET_BOTH_RANGE, + get_both_range = 3, + + /// Position at specified key + set = 15, + /// Position at specified key, return key + data + set_key = 16, + /// Position at first key greater than or equal to specified key. + set_range = 17, +}; + +/// Cursor Get operations that do *not* require a key to execute a lookup +const CursorRelativeOperation = enum(c_uint) { + /// Position at first key/data item + first = 0, + /// Position at first data item of current key. Only for #MDB_DUPSORT + first_dup = 1, + /// Return key/data at current cursor position - GET_CURRENT, + get_current = 4, /// Return up to a page of duplicate data items from current cursor position. /// Move cursor to prepare for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED - GET_MULTIPLE, + get_multiple = 5, /// Position at last key/data item - LAST, + last = 6, /// Position at last data item of current key. Only for #MDB_DUPSORT - LAST_DUP, + last_dup = 7, /// Position at next data item - NEXT, + next = 8, /// Position at next data item of current key. Only for #MDB_DUPSORT - NEXT_DUP, + next_dup = 9, /// Return up to a page of duplicate data items from next cursor position. /// Move cursor to prepare for #MDB_NEXT_MULTIPLE. Only for #MDB_DUPFIXED - NEXT_MULTIPLE, + next_multiple = 10, /// Position at first data item of next key - NEXT_NODUP, + next_nodup = 11, /// Position at previous data item - PREV, + prev = 12, /// Position at previous data item of current key. Only for #MDB_DUPSORT - PREV_DUP, + prev_dup = 13, /// Position at last data item of previous key - PREV_NODUP, - /// Position at specified key - SET, - /// Position at specified key, return key + data - SET_KEY, - /// Position at first key greater than or equal to specified key. - SET_RANGE, + prev_nodup = 14, + /// Position at previous page and return up to a page of duplicate data items. /// Only for #MDB_DUPFIXED - PREV_MULTIPLE, + prev_multiple = 18, }; fn result(int: isize) LmdbError!void { From 4c9a312d8f80f0e28297769f8badb98bb54cbc14 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 18:58:59 -0400 Subject: [PATCH 06/32] fix(ledger): remove unused --- src/ledger/database/lmdb.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index ff1bde1d9..629ad3340 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -9,7 +9,6 @@ const BytesRef = database.interface.BytesRef; const ColumnFamily = database.interface.ColumnFamily; const IteratorDirection = database.interface.IteratorDirection; const Logger = sig.trace.Logger; -const ReturnType = sig.utils.types.ReturnType; const key_serializer = database.interface.key_serializer; const value_serializer = database.interface.value_serializer; From 8be72a084b62863734b5161d76dc4d7557f37342 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 19:40:50 -0400 Subject: [PATCH 07/32] fix(ledger): misc bugs found when testing as blockstore db --- src/ledger/database/lmdb.zig | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 629ad3340..7a4fedd55 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -68,18 +68,13 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { return self.dbis[cf.find(column_families)]; } - pub fn count(self: *Self, comptime cf: ColumnFamily) Allocator.Error!u64 { - const live_files = try self.db.liveFiles(self.allocator); - defer live_files.deinit(); - - var sum: u64 = 0; - for (live_files.items) |live_file| { - if (std.mem.eql(u8, live_file.column_family_name, cf.name)) { - sum += live_file.num_entries; - } - } + pub fn count(self: *Self, comptime cf: ColumnFamily) LmdbOrAllocatorError!u64 { + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + defer c.mdb_txn_abort(txn); - return sum; + const stat = try ret(c.mdb_stat, .{ txn, self.dbi(cf) }); + + return stat.ms_entries; } pub fn put( @@ -131,7 +126,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); errdefer c.mdb_txn_abort(txn); - const item = try ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { + const item = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { error.MDB_NOTFOUND => return null, else => return e, }; @@ -347,7 +342,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn nextValue(self: *@This()) anyerror!?cf.Value { _, const val = try self.nextImpl() orelse return null; - return try key_serializer.deserialize(cf.Key, self.allocator, val); + return try key_serializer.deserialize(cf.Value, self.allocator, val); } /// Returned data does not outlive the iterator. @@ -381,13 +376,14 @@ fn fromVal(value: c.MDB_val) []const u8 { } fn txnResetter(txn: *c.MDB_txn) Allocator { + const vtable = .{ + .alloc = &sig.utils.allocators.noAlloc, + .resize = &Allocator.noResize, + .free = &resetTxnFree, + }; return .{ .ptr = @ptrCast(@alignCast(txn)), - .vtable = .{ - .alloc = &sig.utils.allocators.noAlloc, - .resize = &Allocator.noResize, - .free = &resetTxnFree, - }, + .vtable = &vtable, }; } @@ -577,6 +573,8 @@ fn result(int: isize) LmdbError!void { }; } +pub const LmdbOrAllocatorError = LmdbError || Allocator.Error; + pub const LmdbError = error{ //////////////////////////////////////////////////////// /// lmdb-specific errors From d03475cdf4a3e6881ab1a8a0ae94875f629e9a9e Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 19:31:48 -0400 Subject: [PATCH 08/32] feat(ledger): customizable blockstore database backend --- build.zig | 25 +++++++++++++++++++++---- src/ledger/blockstore.zig | 6 +++++- src/ledger/database/hashmap.zig | 3 ++- src/ledger/database/lib.zig | 3 ++- src/ledger/database/lmdb.zig | 3 ++- src/ledger/database/rocksdb.zig | 1 + 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index 94d5667c6..009a51415 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,11 @@ pub fn build(b: *Build) void { const filters = b.option([]const []const u8, "filter", "List of filters, used for example to filter unit tests by name"); // specified as a series like `-Dfilter="filter1" -Dfilter="filter2"` const enable_tsan = b.option(bool, "enable-tsan", "Enable TSan for the test suite"); const no_run = b.option(bool, "no-run", "Do not run the selected step and install it") orelse false; + const blockstore_db = b.option(BlockstoreDB, "blockstore-db", "Blockstore database backend") orelse .rocksdb; + + // Build options + const build_options = b.addOptions(); + build_options.addOption(BlockstoreDB, "blockstore_db", blockstore_db); // CLI build steps const sig_step = b.step("run", "Run the sig executable"); @@ -58,8 +63,9 @@ pub fn build(b: *Build) void { sig_mod.addImport("httpz", httpz_mod); sig_mod.addImport("zstd", zstd_mod); sig_mod.addImport("curl", curl_mod); - sig_mod.addImport("rocksdb", rocksdb_mod); - sig_mod.addImport("lmdb", lmdb_mod); + if (blockstore_db == .rocksdb) sig_mod.addImport("rocksdb", rocksdb_mod); + if (blockstore_db == .lmdb) sig_mod.addImport("lmdb", lmdb_mod); + sig_mod.addOptions("build-options", build_options); // main executable const sig_exe = b.addExecutable(.{ @@ -76,8 +82,9 @@ pub fn build(b: *Build) void { sig_exe.root_module.addImport("zig-cli", zig_cli_module); sig_exe.root_module.addImport("zig-network", zig_network_module); sig_exe.root_module.addImport("zstd", zstd_mod); - sig_exe.root_module.addImport("rocksdb", rocksdb_mod); - sig_exe.root_module.addImport("lmdb", lmdb_mod); + if (blockstore_db == .rocksdb) sig_exe.root_module.addImport("rocksdb", rocksdb_mod); + if (blockstore_db == .lmdb) sig_exe.root_module.addImport("lmdb", lmdb_mod); + sig_exe.root_module.addOptions("build-options", build_options); sig_exe.linkLibC(); const main_exe_run = b.addRunArtifact(sig_exe); @@ -117,6 +124,7 @@ pub fn build(b: *Build) void { unit_tests_exe.root_module.addImport("zstd", zstd_mod); unit_tests_exe.root_module.addImport("rocksdb", rocksdb_mod); unit_tests_exe.root_module.addImport("lmdb", lmdb_mod); + unit_tests_exe.root_module.addOptions("build-options", build_options); unit_tests_exe.linkLibC(); const unit_tests_exe_run = b.addRunArtifact(unit_tests_exe); @@ -159,6 +167,9 @@ pub fn build(b: *Build) void { benchmark_exe.root_module.addImport("rocksdb", rocksdb_mod); benchmark_exe.root_module.addImport("lmdb", lmdb_mod); benchmark_exe.root_module.addImport("prettytable", pretty_table_mod); + if (blockstore_db == .rocksdb) sig_exe.root_module.addImport("rocksdb", rocksdb_mod); + if (blockstore_db == .lmdb) sig_exe.root_module.addImport("lmdb", lmdb_mod); + benchmark_exe.root_module.addOptions("build-options", build_options); benchmark_exe.linkLibC(); const benchmark_exe_run = b.addRunArtifact(benchmark_exe); @@ -202,3 +213,9 @@ fn makeZlsNotInstallAnythingDuringBuildOnSave(b: *Build) void { artifact.generated_bin = null; } } + +const BlockstoreDB = enum { + rocksdb, + lmdb, + hashmap, +}; diff --git a/src/ledger/blockstore.zig b/src/ledger/blockstore.zig index ddc977cad..e3e45189a 100644 --- a/src/ledger/blockstore.zig +++ b/src/ledger/blockstore.zig @@ -1,6 +1,10 @@ const ledger = @import("lib.zig"); -pub const BlockstoreDB = ledger.database.RocksDB(&ledger.schema.list); +pub const BlockstoreDB = switch (@import("build-options").blockstore_db) { + .rocksdb => ledger.database.RocksDB(&ledger.schema.list), + .hashmap => ledger.database.SharedHashMapDB(&ledger.schema.list), + .lmdb => ledger.database.LMDB(&ledger.schema.list), +}; test BlockstoreDB { ledger.database.assertIsDatabase(BlockstoreDB); diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index edd12011f..dd4609b0c 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -26,9 +26,10 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { pub fn open( allocator: Allocator, - _: Logger, + logger: Logger, _: []const u8, ) Allocator.Error!Self { + logger.info().log("Initializing SharedHashMapDB"); var maps = try allocator.alloc(SharedHashMap, column_families.len); errdefer { for (maps) |*m| m.deinit(); diff --git a/src/ledger/database/lib.zig b/src/ledger/database/lib.zig index afc48fd7c..cfc47277b 100644 --- a/src/ledger/database/lib.zig +++ b/src/ledger/database/lib.zig @@ -6,8 +6,9 @@ pub const rocksdb = @import("rocksdb.zig"); pub const BytesRef = interface.BytesRef; pub const ColumnFamily = interface.ColumnFamily; pub const Database = interface.Database; -pub const SharedHashMapDB = hashmap.SharedHashMapDB; +pub const LMDB = lmdb.LMDB; pub const RocksDB = rocksdb.RocksDB; +pub const SharedHashMapDB = hashmap.SharedHashMapDB; pub const assertIsDatabase = interface.assertIsDatabase; diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 7a4fedd55..9886000fd 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -22,7 +22,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const Self = @This(); - pub fn open(allocator: Allocator, _: Logger, path: []const u8) anyerror!Self { + pub fn open(allocator: Allocator, logger: Logger, path: []const u8) anyerror!Self { + logger.info().log("Initializing LMDB"); const owned_path = try allocator.dupe(u8, path); // create and open the database diff --git a/src/ledger/database/rocksdb.zig b/src/ledger/database/rocksdb.zig index 65e2e547f..d77a3a23c 100644 --- a/src/ledger/database/rocksdb.zig +++ b/src/ledger/database/rocksdb.zig @@ -25,6 +25,7 @@ pub fn RocksDB(comptime column_families: []const ColumnFamily) type { const Self = @This(); pub fn open(allocator: Allocator, logger: Logger, path: []const u8) Error!Self { + logger.info().log("Initializing RocksDB"); const owned_path = try allocator.dupe(u8, path); // allocate cf descriptions From d4682f10b7ce3c25fd4575913b1f0eb0eb573168 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Fri, 1 Nov 2024 19:53:21 -0400 Subject: [PATCH 09/32] fix(ledger): hashmapdb compile error when used as blockstore database --- src/ledger/database/hashmap.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index dd4609b0c..096757408 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -360,12 +360,12 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { pub fn nextKey(self: *@This()) anyerror!?cf.Key { const index = self.nextIndex() orelse return null; - return key_serializer.deserialize(cf.Key, self.allocator, self.keys[index]); + return try key_serializer.deserialize(cf.Key, self.allocator, self.keys[index]); } pub fn nextValue(self: *@This()) anyerror!?cf.Value { const index = self.nextIndex() orelse return null; - return value_serializer.deserialize(cf.Value, self.allocator, self.vals[index]); + return try value_serializer.deserialize(cf.Value, self.allocator, self.vals[index]); } pub fn nextBytes(self: *@This()) error{}!?[2]BytesRef { From e1a261c6aec849b4f1f8df3f7aed8cc3ac929cad Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Mon, 4 Nov 2024 12:18:09 -0500 Subject: [PATCH 10/32] test(ledger): migrate new database tests to normal zig tests --- src/ledger/database/interface.zig | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index b05283406..fc918a712 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -537,7 +537,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator forward start before all"() !void { + test "iterator forward start before all" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -567,7 +568,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator forward start after all"() !void { + test "iterator forward start after all" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -584,7 +586,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator reverse start before all"() !void { + test "iterator reverse start before all" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -614,7 +617,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator reverse start after all"() !void { + test "iterator reverse start after all" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -631,7 +635,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator forward empty"() !void { + test "iterator forward empty" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -643,7 +648,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator reverse empty"() !void { + test "iterator reverse empty" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -655,7 +661,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator forward empty with null start"() !void { + test "iterator forward empty with null start" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -667,7 +674,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"iterator reverse empty with null start"() !void { + test "iterator reverse empty with null start" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); @@ -679,7 +687,8 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try iter.next()); } - pub fn @"WriteBatch.deleteRange"() !void { + test "WriteBatch.deleteRange" { + const allocator = std.testing.allocator; const path = test_dir ++ @src().fn_name; try ledger.tests.freshDir(path); var db = try DB.open(allocator, logger, path); From 6f3bb7b2e1a3e265029afd37b715b737008cdcaf Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 11:01:50 -0500 Subject: [PATCH 11/32] refactor(build.zig): use switch instead of ifs for database dependency --- build.zig | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index 009a51415..b99cb0cab 100644 --- a/build.zig +++ b/build.zig @@ -63,8 +63,11 @@ pub fn build(b: *Build) void { sig_mod.addImport("httpz", httpz_mod); sig_mod.addImport("zstd", zstd_mod); sig_mod.addImport("curl", curl_mod); - if (blockstore_db == .rocksdb) sig_mod.addImport("rocksdb", rocksdb_mod); - if (blockstore_db == .lmdb) sig_mod.addImport("lmdb", lmdb_mod); + switch (blockstore_db) { + .rocksdb => sig_mod.addImport("rocksdb", rocksdb_mod), + .lmdb => sig_mod.addImport("lmdb", lmdb_mod), + .hashmap => {}, + } sig_mod.addOptions("build-options", build_options); // main executable @@ -82,8 +85,11 @@ pub fn build(b: *Build) void { sig_exe.root_module.addImport("zig-cli", zig_cli_module); sig_exe.root_module.addImport("zig-network", zig_network_module); sig_exe.root_module.addImport("zstd", zstd_mod); - if (blockstore_db == .rocksdb) sig_exe.root_module.addImport("rocksdb", rocksdb_mod); - if (blockstore_db == .lmdb) sig_exe.root_module.addImport("lmdb", lmdb_mod); + switch (blockstore_db) { + .rocksdb => sig_exe.root_module.addImport("rocksdb", rocksdb_mod), + .lmdb => sig_exe.root_module.addImport("lmdb", lmdb_mod), + .hashmap => {}, + } sig_exe.root_module.addOptions("build-options", build_options); sig_exe.linkLibC(); @@ -164,11 +170,12 @@ 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("rocksdb", rocksdb_mod); - benchmark_exe.root_module.addImport("lmdb", lmdb_mod); benchmark_exe.root_module.addImport("prettytable", pretty_table_mod); - if (blockstore_db == .rocksdb) sig_exe.root_module.addImport("rocksdb", rocksdb_mod); - if (blockstore_db == .lmdb) sig_exe.root_module.addImport("lmdb", lmdb_mod); + switch (blockstore_db) { + .rocksdb => benchmark_exe.root_module.addImport("rocksdb", rocksdb_mod), + .lmdb => benchmark_exe.root_module.addImport("lmdb", lmdb_mod), + .hashmap => {}, + } benchmark_exe.root_module.addOptions("build-options", build_options); benchmark_exe.linkLibC(); From 876eb2def13c70bb4c489afc28e7323d170e2d96 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 11:06:54 -0500 Subject: [PATCH 12/32] refactor(ledger): extract out import to top --- src/ledger/blockstore.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ledger/blockstore.zig b/src/ledger/blockstore.zig index e3e45189a..d0391b8b9 100644 --- a/src/ledger/blockstore.zig +++ b/src/ledger/blockstore.zig @@ -1,6 +1,7 @@ +const build_options = @import("build-options"); const ledger = @import("lib.zig"); -pub const BlockstoreDB = switch (@import("build-options").blockstore_db) { +pub const BlockstoreDB = switch (build_options.blockstore_db) { .rocksdb => ledger.database.RocksDB(&ledger.schema.list), .hashmap => ledger.database.SharedHashMapDB(&ledger.schema.list), .lmdb => ledger.database.LMDB(&ledger.schema.list), From 79427fc9d9acad29a6b9419a4815567e0a4f1361 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 11:28:43 -0500 Subject: [PATCH 13/32] refactor(ledger): remove c pointers from lmdb --- src/ledger/database/lmdb.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 9886000fd..f389b57ea 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -44,7 +44,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // open cf/database, creating if necessary dbis[i] = try ret(c.mdb_dbi_open, .{ txn, - @as([*c]const u8, @ptrCast(cf.name)), + @as([*]const u8, @ptrCast(cf.name)), MDB_CREATE, }); } @@ -372,7 +372,7 @@ fn toVal(bytes: []const u8) c.MDB_val { } fn fromVal(value: c.MDB_val) []const u8 { - const ptr: [*c]u8 = @ptrCast(value.mv_data); + const ptr: [*]u8 = @ptrCast(value.mv_data); return ptr[0..value.mv_size]; } From 1c225918e3332d1be0110d9722dafe59cfc6d1fa Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 11:39:31 -0500 Subject: [PATCH 14/32] refactor(ledger): use flags from c import --- src/ledger/database/lmdb.zig | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index f389b57ea..6e20cac65 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -45,7 +45,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { dbis[i] = try ret(c.mdb_dbi_open, .{ txn, @as([*]const u8, @ptrCast(cf.name)), - MDB_CREATE, + c.MDB_CREATE, }); } @@ -70,7 +70,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } pub fn count(self: *Self, comptime cf: ColumnFamily) LmdbOrAllocatorError!u64 { - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); defer c.mdb_txn_abort(txn); const stat = try ret(c.mdb_stat, .{ txn, self.dbi(cf) }); @@ -108,7 +108,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); defer c.mdb_txn_abort(txn); const value = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { @@ -124,7 +124,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, MDB_RDONLY }); + const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); errdefer c.mdb_txn_abort(txn); const item = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { @@ -420,9 +420,6 @@ fn IntermediateType(function: anytype) type { return @typeInfo(params[params.len - 1].type.?).Pointer.child; } -const MDB_CREATE = 0x40000; -const MDB_RDONLY = 0x20000; - fn cursorGet( cursor: *c.MDB_cursor, key: []const u8, From cd49700cd5807271b066a156816d67e4f672d36e Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 14:24:59 -0500 Subject: [PATCH 15/32] fix(ledger): lmdb error conversion needs platform specific logic --- src/ledger/database/lmdb.zig | 123 +---------------------------------- src/utils/errors.zig | 62 ++++++++++++++++++ src/utils/lib.zig | 1 + 3 files changed, 65 insertions(+), 121 deletions(-) create mode 100644 src/utils/errors.zig diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 6e20cac65..51042ce22 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -511,7 +511,6 @@ const CursorRelativeOperation = enum(c_uint) { fn result(int: isize) LmdbError!void { return switch (int) { - 0 => {}, -30799 => error.MDB_KEYEXIST, -30798 => error.MDB_NOTFOUND, -30797 => error.MDB_PAGE_NOTFOUND, @@ -533,51 +532,13 @@ fn result(int: isize) LmdbError!void { -30781 => error.MDB_BAD_VALSIZE, -30780 => error.MDB_BAD_DBI, -30779 => error.MDB_PROBLEM, - 1 => error.EPERM, - 2 => error.ENOENT, - 3 => error.ESRCH, - 4 => error.EINTR, - 5 => error.EIO, - 6 => error.ENXIO, - 7 => error.E2BIG, - 8 => error.ENOEXEC, - 9 => error.EBADF, - 10 => error.ECHILD, - 11 => error.EAGAIN, - 12 => error.ENOMEM, - 13 => error.EACCES, - 14 => error.EFAULT, - 15 => error.ENOTBLK, - 16 => error.EBUSY, - 17 => error.EEXIST, - 18 => error.EXDEV, - 19 => error.ENODEV, - 20 => error.ENOTDIR, - 21 => error.EISDIR, - 22 => error.EINVAL, - 23 => error.ENFILE, - 24 => error.EMFILE, - 25 => error.ENOTTY, - 26 => error.ETXTBSY, - 27 => error.EFBIG, - 28 => error.ENOSPC, - 29 => error.ESPIPE, - 30 => error.EROFS, - 31 => error.EMLINK, - 32 => error.EPIPE, - 33 => error.EDOM, - 34 => error.ERANGE, - else => error.UnspecifiedErrorCode, + else => sig.utils.errors.errnoToError(@enumFromInt(int)), }; } pub const LmdbOrAllocatorError = LmdbError || Allocator.Error; -pub const LmdbError = error{ - //////////////////////////////////////////////////////// - /// lmdb-specific errors - //// - +pub const LmdbError = sig.utils.errors.LibcError || error{ /// Successful result MDB_SUCCESS, /// key/data pair already exists @@ -626,86 +587,6 @@ pub const LmdbError = error{ MDB_BAD_DBI, /// Unexpected problem - txn should abort MDB_PROBLEM, - - //////////////////////////////////////////////////////// - /// asm-generic errors - may be thrown by lmdb - //// - - /// Operation not permitted - EPERM, - /// No such file or directory - ENOENT, - /// No such process - ESRCH, - /// Interrupted system call - EINTR, - /// I/O error - EIO, - /// No such device or address - ENXIO, - /// Argument list too long - E2BIG, - /// Exec format error - ENOEXEC, - /// Bad file number - EBADF, - /// No child processes - ECHILD, - /// Try again - EAGAIN, - /// Out of memory - ENOMEM, - /// Permission denied - EACCES, - /// Bad address - EFAULT, - /// Block device required - ENOTBLK, - /// Device or resource busy - EBUSY, - /// File exists - EEXIST, - /// Cross-device link - EXDEV, - /// No such device - ENODEV, - /// Not a directory - ENOTDIR, - /// Is a directory - EISDIR, - /// Invalid argument - EINVAL, - /// File table overflow - ENFILE, - /// Too many open files - EMFILE, - /// Not a typewriter - ENOTTY, - /// Text file busy - ETXTBSY, - /// File too large - EFBIG, - /// No space left on device - ENOSPC, - /// Illegal seek - ESPIPE, - /// Read-only file system - EROFS, - /// Too many links - EMLINK, - /// Broken pipe - EPIPE, - /// Math argument out of domain of func - EDOM, - /// Math result not representable - ERANGE, - - //////////////////////////////////////////////////////// - /// errors interfacing with Lmdb - //// - - /// Got a return value that is not specified in LMDB's header files - UnspecifiedErrorCode, }; comptime { diff --git a/src/utils/errors.zig b/src/utils/errors.zig new file mode 100644 index 000000000..90d4c47df --- /dev/null +++ b/src/utils/errors.zig @@ -0,0 +1,62 @@ +const std = @import("std"); + +/// Error set that includes every variant defined in std.posix.system.E, with two exceptions: +/// - excludes `SUCCESS` +/// - includes `UnknownErrno` since `E` is not exhaustive +/// +/// Explicitly defined, it would look something like this: +/// ```zig +/// pub const LibcError = error { +/// PERM, +/// NOENT, +/// SRCH, +/// INTR, +/// IO, +/// ... many more items ... +/// UnknownErrno, +/// }; +/// ``` +pub const LibcError: type = blk: { + const enum_variants = @typeInfo(std.posix.E).Enum.fields; + var error_set: [enum_variants.len]std.builtin.Type.Error = undefined; + for (enum_variants[1..], 0..) |enum_variant, i| { + error_set[i].name = enum_variant.name; + } + error_set[enum_variants.len - 1].name = "UnknownErrno"; + break :blk @Type(.{ .ErrorSet = &error_set }); +}; + +/// Converts errno enum into an error union. +/// - void for SUCCESS +/// - SystemError for any other value +pub fn errnoToError(errno: std.posix.E) LibcError!void { + if (errno == .SUCCESS) return; + + const enum_variants = @typeInfo(std.posix.E).Enum.fields; + const Entry = struct { u16, LibcError }; + const map: [enum_variants.len - 1]Entry = comptime blk: { + var map: [enum_variants.len - 1]Entry = undefined; + for (enum_variants[1..], 0..) |enum_variant, i| { + std.debug.assert(enum_variant.value > enum_variants[i].value); + map[i] = .{ enum_variant.value, @field(LibcError, enum_variant.name) }; + } + break :blk map; + }; + + const search_result = std.sort.binarySearch(Entry, @intFromEnum(errno), &map, {}, struct { + fn compareFn(_: void, key: u16, mid_item: Entry) std.math.Order { + return std.math.order(key, mid_item[0]); + } + }.compareFn); + + return if (search_result) |entry| + map[entry][1] + else + LibcError.UnknownErrno; +} + +test errnoToError { + try errnoToError(std.posix.E.SUCCESS); + try std.testing.expectError(LibcError.MFILE, errnoToError(std.posix.E.MFILE)); + try std.testing.expectError(LibcError.NOMSG, errnoToError(std.posix.E.NOMSG)); +} diff --git a/src/utils/lib.zig b/src/utils/lib.zig index e76209d2f..e68cf0f7b 100644 --- a/src/utils/lib.zig +++ b/src/utils/lib.zig @@ -3,6 +3,7 @@ pub const collections = @import("collections.zig"); pub const closure = @import("closure.zig"); pub const bitflags = @import("bitflags.zig"); pub const directory = @import("directory.zig"); +pub const errors = @import("errors.zig"); pub const interface = @import("interface.zig"); pub const io = @import("io.zig"); pub const lazy = @import("lazy.zig"); From c01b74f6ecc41c8cfea5c1d228abe49cd2c1f1e4 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 14:35:28 -0500 Subject: [PATCH 16/32] refactor(ledger): remove unnecessary ptrCast from lmdb --- src/ledger/database/lmdb.zig | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 51042ce22..10459a2a7 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -42,11 +42,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // save cf handles inline for (column_families, 0..) |cf, i| { // open cf/database, creating if necessary - dbis[i] = try ret(c.mdb_dbi_open, .{ - txn, - @as([*]const u8, @ptrCast(cf.name)), - c.MDB_CREATE, - }); + dbis[i] = try ret(c.mdb_dbi_open, .{ txn, cf.name.ptr, c.MDB_CREATE }); } // persist column families From 54975b2c25e4f76b2241599e3996865552c8bb18 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 14:39:48 -0500 Subject: [PATCH 17/32] refactor(ledger): rename lmdb "result" to "maybeError" --- src/ledger/database/lmdb.zig | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 10459a2a7..e73fb6993 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -28,8 +28,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // create and open the database const env = try ret(c.mdb_env_create, .{}); - try result(c.mdb_env_set_maxdbs(env, column_families.len)); - try result(c.mdb_env_open(env, @ptrCast(path), 0, 0o700)); + try maybeError(c.mdb_env_set_maxdbs(env, column_families.len)); + try maybeError(c.mdb_env_open(env, @ptrCast(path), 0, 0o700)); // begin transaction to create column families aka "databases" in lmdb const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); @@ -46,7 +46,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } // persist column families - try result(c.mdb_txn_commit(txn)); + try maybeError(c.mdb_txn_commit(txn)); return .{ .allocator = allocator, @@ -90,8 +90,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); - try result(c.mdb_put(txn, self.dbi(cf), &key_val, &val_val, 0)); - try result(c.mdb_txn_commit(txn)); + try maybeError(c.mdb_put(txn, self.dbi(cf), &key_val, &val_val, 0)); + try maybeError(c.mdb_txn_commit(txn)); } pub fn get( @@ -147,11 +147,11 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_abort(txn); - result(c.mdb_del(txn, self.dbi(cf), &key_val, &val_val)) catch |e| switch (e) { + maybeError(c.mdb_del(txn, self.dbi(cf), &key_val, &val_val)) catch |e| switch (e) { error.MDB_NOTFOUND => {}, else => return e, }; - try result(c.mdb_txn_commit(txn)); + try maybeError(c.mdb_txn_commit(txn)); } pub fn deleteFilesInRange( @@ -179,7 +179,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } pub fn commit(_: *Self, batch: WriteBatch) LmdbError!void { - try result(c.mdb_txn_commit(batch.txn)); + try maybeError(c.mdb_txn_commit(batch.txn)); } /// A write batch is a sequence of operations that execute atomically. @@ -220,7 +220,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); - try result(c.mdb_put(self.txn, self.dbi(cf), &key_val, &val_val, 0)); + try maybeError(c.mdb_put(self.txn, self.dbi(cf), &key_val, &val_val, 0)); } pub fn delete( @@ -232,7 +232,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - try result(c.mdb_del(self.txn, self.dbi(cf), &key_val, 0)); + try maybeError(c.mdb_del(self.txn, self.dbi(cf), &key_val, 0)); } pub fn deleteRange( @@ -255,7 +255,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { return; while (std.mem.lessThan(u8, key, end_bytes.data)) { - try result(c.mdb_cursor_del(cursor, 0)); + try maybeError(c.mdb_cursor_del(cursor, 0)); key, _ = try cursorGetRelative(cursor, .next) orelse return; } } @@ -396,7 +396,7 @@ fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) .Int => 0, else => undefined, }; - try result(@call(.auto, constructor, args ++ .{&maybe})); + try maybeError(@call(.auto, constructor, args ++ .{&maybe})); return switch (@typeInfo(Intermediate)) { .Optional => maybe.?, else => maybe, @@ -423,7 +423,7 @@ fn cursorGet( ) LmdbError!?struct { []const u8, []const u8 } { var key_val = toVal(key); var val_val: c.MDB_val = undefined; - result(c.mdb_cursor_get( + maybeError(c.mdb_cursor_get( cursor, &key_val, &val_val, @@ -441,7 +441,7 @@ fn cursorGetRelative( ) LmdbError!?struct { []const u8, []const u8 } { var key_val: c.MDB_val = undefined; var val_val: c.MDB_val = undefined; - result(c.mdb_cursor_get( + maybeError(c.mdb_cursor_get( cursor, &key_val, &val_val, @@ -505,7 +505,8 @@ const CursorRelativeOperation = enum(c_uint) { prev_multiple = 18, }; -fn result(int: isize) LmdbError!void { +/// Converts an error return code from LMDB into an error union +fn maybeError(int: isize) LmdbError!void { return switch (int) { -30799 => error.MDB_KEYEXIST, -30798 => error.MDB_NOTFOUND, From 11cd6ccdf79e6b1cc2e60d9a894cb1ce8d977a20 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 14:49:42 -0500 Subject: [PATCH 18/32] fix(ledger): strings should be 0-terminated --- src/ledger/database/interface.zig | 2 +- src/ledger/database/lmdb.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index fc918a712..67ddef66d 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -177,7 +177,7 @@ pub fn Database(comptime Impl: type) type { pub const IteratorDirection = enum { forward, reverse }; pub const ColumnFamily = struct { - name: []const u8, + name: [:0]const u8, Key: type, Value: type, diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index e73fb6993..67ceda824 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -18,18 +18,18 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { allocator: Allocator, env: *c.MDB_env, dbis: []const c.MDB_dbi, - path: []const u8, + path: [:0]const u8, const Self = @This(); pub fn open(allocator: Allocator, logger: Logger, path: []const u8) anyerror!Self { logger.info().log("Initializing LMDB"); - const owned_path = try allocator.dupe(u8, path); + const owned_path = try allocator.dupeZ(u8, path); // create and open the database const env = try ret(c.mdb_env_create, .{}); try maybeError(c.mdb_env_set_maxdbs(env, column_families.len)); - try maybeError(c.mdb_env_open(env, @ptrCast(path), 0, 0o700)); + try maybeError(c.mdb_env_open(env, owned_path.ptr, 0, 0o700)); // begin transaction to create column families aka "databases" in lmdb const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); From e9d84f956cd342a673835882ce65c34790e938e4 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 15:32:23 -0500 Subject: [PATCH 19/32] docs(ledger): document txn-based allocator for lmdb --- src/ledger/database/lmdb.zig | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 67ceda824..8289f401e 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -129,7 +129,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { }; return .{ - .allocator = txnResetter(txn), + .allocator = txnAborter(txn), .data = fromVal(item), }; } @@ -372,7 +372,16 @@ fn fromVal(value: c.MDB_val) []const u8 { return ptr[0..value.mv_size]; } -fn txnResetter(txn: *c.MDB_txn) Allocator { +/// Returns an `Allocator` that frees memory by aborting the transaction +/// that owns the memory. It cannot allocate anything. +/// +/// This exists to be the Allocator used in a `BytesRef` instance +/// +/// Calling `free` with any input will free all memory that was allocated +/// by the transaction. This means you cannot manage lifetimes of multiple +/// items separately. Ideally you would only use this when you've only +/// read exactly one item in the transaction. +fn txnAborter(txn: *c.MDB_txn) Allocator { const vtable = .{ .alloc = &sig.utils.allocators.noAlloc, .resize = &Allocator.noResize, From 2ed42a065e227c9679e5da8954535560365b4404 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 16:28:16 -0500 Subject: [PATCH 20/32] refactor(ledger): rename ret to returnOutput and add docs --- src/ledger/database/lmdb.zig | 88 ++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 8289f401e..e503005f7 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -27,12 +27,12 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const owned_path = try allocator.dupeZ(u8, path); // create and open the database - const env = try ret(c.mdb_env_create, .{}); + const env = try returnOutput(c.mdb_env_create, .{}); try maybeError(c.mdb_env_set_maxdbs(env, column_families.len)); try maybeError(c.mdb_env_open(env, owned_path.ptr, 0, 0o700)); // begin transaction to create column families aka "databases" in lmdb - const txn = try ret(c.mdb_txn_begin, .{ env, null, 0 }); + const txn = try returnOutput(c.mdb_txn_begin, .{ env, null, 0 }); errdefer c.mdb_txn_abort(txn); // allocate cf handles @@ -42,7 +42,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // save cf handles inline for (column_families, 0..) |cf, i| { // open cf/database, creating if necessary - dbis[i] = try ret(c.mdb_dbi_open, .{ txn, cf.name.ptr, c.MDB_CREATE }); + dbis[i] = try returnOutput(c.mdb_dbi_open, .{ txn, cf.name.ptr, c.MDB_CREATE }); } // persist column families @@ -66,10 +66,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { } pub fn count(self: *Self, comptime cf: ColumnFamily) LmdbOrAllocatorError!u64 { - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); defer c.mdb_txn_abort(txn); - const stat = try ret(c.mdb_stat, .{ txn, self.dbi(cf) }); + const stat = try returnOutput(c.mdb_stat, .{ txn, self.dbi(cf) }); return stat.ms_entries; } @@ -85,7 +85,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const val_bytes = try value_serializer.serializeToRef(self.allocator, value); defer val_bytes.deinit(); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_abort(txn); var key_val = toVal(key_bytes.data); @@ -104,10 +104,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); defer c.mdb_txn_abort(txn); - const value = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { + const value = returnOutput(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { error.MDB_NOTFOUND => return null, else => return e, }; @@ -120,10 +120,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer key_bytes.deinit(); var key_val = toVal(key_bytes.data); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); errdefer c.mdb_txn_abort(txn); - const item = ret(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { + const item = returnOutput(c.mdb_get, .{ txn, self.dbi(cf), &key_val }) catch |e| switch (e) { error.MDB_NOTFOUND => return null, else => return e, }; @@ -144,7 +144,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var key_val = toVal(key_bytes.data); var val_val: c.MDB_val = undefined; - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_abort(txn); maybeError(c.mdb_del(txn, self.dbi(cf), &key_val, &val_val)) catch |e| switch (e) { @@ -172,7 +172,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { executed.* = false; return .{ .allocator = self.allocator, - .txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }), + .txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }), .dbis = self.dbis, .executed = executed, }; @@ -246,7 +246,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { const end_bytes = try key_serializer.serializeToRef(self.allocator, end); defer end_bytes.deinit(); - const cursor = try ret(c.mdb_cursor_open, .{ self.txn, self.dbi(cf) }); + const cursor = try returnOutput(c.mdb_cursor_open, .{ self.txn, self.dbi(cf) }); defer c.mdb_cursor_close(cursor); var key, _ = if (try cursorGet(cursor, start_bytes.data, .set_range)) |kv| @@ -273,10 +273,10 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { null; defer if (maybe_start_bytes) |sb| sb.deinit(); - const txn = try ret(c.mdb_txn_begin, .{ self.env, null, 0 }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); errdefer c.mdb_txn_abort(txn); - const cursor = try ret(c.mdb_cursor_open, .{ txn, self.dbi(cf) }); + const cursor = try returnOutput(c.mdb_cursor_open, .{ txn, self.dbi(cf) }); errdefer c.mdb_cursor_close(cursor); var start_operation: CursorRelativeOperation = .get_current; @@ -398,29 +398,67 @@ fn resetTxnFree(ctx: *anyopaque, _: []u8, _: u8, _: usize) void { c.mdb_txn_abort(txn); } -fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) { - const Intermediate = IntermediateType(constructor); - var maybe: IntermediateType(constructor) = switch (@typeInfo(Intermediate)) { +/// Call an LMDB function and return its combined output as an error union. +/// +/// This converts parameter-based outputs into return values, and converts error +/// code integers into zig errors. +/// +/// LMDB functions only return integers representing an error code. If the +/// function actually needs to provide more data as an output, the caller needs +/// to pass in a pointer as the final argument to the function. LMDB will write +/// the output data to that pointer. This makes LMDB cumbersome to use because it +/// requires you to have a few extra lines of code every time you call an LMDB +/// function that has any outputs. This function implements this process for you, +/// so you can get the output data as a normal return value. +/// +/// To use it, pass in the LMDB function you'd like to call, and all of the +/// arguments for that function *except the last*. The last argument is for the +/// output, and will be provided by this function. +/// +/// Without `returnOutput`: +/// ```zig +/// const maybe_env: ?*MDB_env = null; +/// try maybeError(c.mdb_env_create(&maybe_env)); +/// const env = maybe_env.?; +/// ``` +/// +/// With `returnOutput`: +/// ```zig +/// const env = try returnOutput(c.mdb_env_create, .{}); +/// ``` +fn returnOutput(fn_with_output: anytype, args: anytype) LmdbError!OutputType(fn_with_output) { + // create a local variable to hold the function's output. + const MaybeOutput = LastParamChild(fn_with_output); + var maybe_output: MaybeOutput = switch (@typeInfo(MaybeOutput)) { .Optional => null, .Int => 0, else => undefined, }; - try maybeError(@call(.auto, constructor, args ++ .{&maybe})); - return switch (@typeInfo(Intermediate)) { - .Optional => maybe.?, - else => maybe, + + // call the function, passing a pointer to the output variable. + // check the return code, and return an error if there was an error. + try maybeError(@call(.auto, fn_with_output, args ++ .{&maybe_output})); + + // return the output value, unwrapping the optional if needed. + return switch (@typeInfo(MaybeOutput)) { + .Optional => maybe_output.?, + else => maybe_output, }; } -fn TypeToCreate(function: anytype) type { - const InnerType = IntermediateType(function); +/// For an LMDB function that provides its output by writing to a pointer, this +/// is the data type of the output. +fn OutputType(fn_with_output: anytype) type { + const InnerType = LastParamChild(fn_with_output); return switch (@typeInfo(InnerType)) { .Optional => |o| o.child, else => InnerType, }; } -fn IntermediateType(function: anytype) type { +/// Returns the child type of the last parameter of a function, +/// assuming that parameter is a pointer. +fn LastParamChild(function: anytype) type { const params = @typeInfo(@TypeOf(function)).Fn.params; return @typeInfo(params[params.len - 1].type.?).Pointer.child; } From 038f9e5f345aa1ea288137df13ef804489eb494e Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:00:55 -0500 Subject: [PATCH 21/32] fix(ledger): reusing hashmap write batch after execution is a bug, and leads to memory leaks --- src/ledger/database/hashmap.zig | 3 +++ src/ledger/reader.zig | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index 096757408..320725844 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -241,6 +241,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { key: cf.Key, value: cf.Value, ) anyerror!void { + std.debug.assert(!self.executed.*); const k_bytes = try key_serializer.serializeAlloc(self.allocator, key); errdefer self.allocator.free(k_bytes); const v_bytes = try value_serializer.serializeAlloc(self.allocator, value); @@ -256,6 +257,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { comptime cf: ColumnFamily, key: cf.Key, ) anyerror!void { + std.debug.assert(!self.executed.*); const k_bytes = try key_serializer.serializeAlloc(self.allocator, key); errdefer self.allocator.free(k_bytes); return try self.instructions.append( @@ -270,6 +272,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { start: cf.Key, end: cf.Key, ) anyerror!void { + std.debug.assert(!self.executed.*); const start_bytes = try key_serializer.serializeAlloc(self.allocator, start); errdefer self.allocator.free(start_bytes); const end_bytes = try key_serializer.serializeAlloc(self.allocator, end); diff --git a/src/ledger/reader.zig b/src/ledger/reader.zig index 95b99d7f3..507a7b987 100644 --- a/src/ledger/reader.zig +++ b/src/ledger/reader.zig @@ -1872,6 +1872,9 @@ test "slotRangeConnected" { } try db.commit(write_batch); + var write_batch2 = try db.initWriteBatch(); + defer write_batch2.deinit(); + const is_connected = try reader.slotRangeConnected(1, 3); try std.testing.expectEqual(true, is_connected); @@ -1880,7 +1883,7 @@ test "slotRangeConnected" { defer slot_meta.deinit(); // ensure isFull() is FALSE slot_meta.last_index = 1; - try write_batch.put(schema.slot_meta, slot_meta.slot, slot_meta); + try write_batch2.put(schema.slot_meta, slot_meta.slot, slot_meta); // this should still pass try std.testing.expectEqual(true, try reader.slotRangeConnected(1, 3)); From 180866025ad7aeb3cf695e04de0ff17b721ac8c2 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:05:29 -0500 Subject: [PATCH 22/32] fix(ledger): leak in purgeSlots --- src/ledger/cleanup_service.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ledger/cleanup_service.zig b/src/ledger/cleanup_service.zig index 2b1043511..e6fae23e1 100644 --- a/src/ledger/cleanup_service.zig +++ b/src/ledger/cleanup_service.zig @@ -204,6 +204,7 @@ fn findSlotsToClean( /// analog to [`run_purge_with_stats`](https://github.com/anza-xyz/agave/blob/26692e666454d340a6691e2483194934e6a8ddfc/ledger/src/blockstore/blockstore_purge.rs#L202) pub fn purgeSlots(db: *BlockstoreDB, from_slot: Slot, to_slot: Slot) !bool { var write_batch = try db.initWriteBatch(); + defer write_batch.deinit(); // the methods used below are exclusive [from_slot, to_slot), so we add 1 to purge inclusive const purge_to_slot = to_slot + 1; @@ -421,6 +422,7 @@ test "purgeSlots" { // write another type var write_batch = try db.initWriteBatch(); + defer write_batch.deinit(); for (0..roots.len + 1) |i| { const merkle_root_meta = sig.ledger.shred.ErasureSetId{ .erasure_set_index = i, From 8add233d50c82410dc334ff08443c0f7ff671ed0 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:14:00 -0500 Subject: [PATCH 23/32] fix(ledger): serializeAlloc for raw bytes should not use bincode --- src/ledger/database/interface.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index 67ddef66d..7d3f1073e 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -207,8 +207,12 @@ fn serializer(endian: std.builtin.Endian) type { return struct { /// Returned slice is owned by the caller. Free with `allocator.free`. pub fn serializeAlloc(allocator: Allocator, item: anytype) ![]const u8 { - const buf = try allocator.alloc(u8, sig.bincode.sizeOf(item, .{})); - return sig.bincode.writeToSlice(buf, item, .{ .endian = endian }); + if (@TypeOf(item) == []const u8 or @TypeOf(item) == []u8) { + return try allocator.dupe(u8, item); + } else { + const buf = try allocator.alloc(u8, sig.bincode.sizeOf(item, .{})); + return sig.bincode.writeToSlice(buf, item, .{ .endian = endian }); + } } /// Returned data may or may not be owned by the caller. From dbe786a385da381adc7cf0aa0ac21ddde8f743cd Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:17:53 -0500 Subject: [PATCH 24/32] fix(ledger): deinit shred bytes in test --- src/ledger/reader.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ledger/reader.zig b/src/ledger/reader.zig index 507a7b987..a56e28ba3 100644 --- a/src/ledger/reader.zig +++ b/src/ledger/reader.zig @@ -2168,10 +2168,11 @@ test "getCodeShred" { try db.commit(write_batch); // correct data read - const read_bytes_ref = try reader.getCodeShred(shred_slot, shred_index) orelse { + const code_shred = try reader.getCodeShred(shred_slot, shred_index) orelse { return error.NullDataShred; }; - try std.testing.expectEqualSlices(u8, shred.payload(), read_bytes_ref.data); + defer code_shred.deinit(); + try std.testing.expectEqualSlices(u8, shred.payload(), code_shred.data); // incorrect slot if (try reader.getCodeShred(shred_slot + 10, shred_index) != null) { @@ -2237,13 +2238,14 @@ test "getDataShred" { try db.commit(write_batch); // correct data read - const read_bytes_ref = try reader.getDataShred( + const data_shred = try reader.getDataShred( shred_slot, shred_index, ) orelse { return error.NullDataShred; }; - try std.testing.expectEqualSlices(u8, shred_payload, read_bytes_ref.data); + defer data_shred.deinit(); + try std.testing.expectEqualSlices(u8, shred_payload, data_shred.data); // incorrect slot if (try reader.getDataShred(shred_slot + 10, shred_index) != null) { From e1d5fdcc692ea8b9c3c1ab2aa679fa45f208a944 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:44:00 -0500 Subject: [PATCH 25/32] fix(ledger): write batch copy/pointer mismanagement the problem was in insertShreds because it was copying the write batch instead of getting a pointer. so it would only insert the things that are inserted by the pending state, not insertShreds --- src/ledger/benchmarks.zig | 2 +- src/ledger/cleanup_service.zig | 6 ++-- src/ledger/database/hashmap.zig | 2 +- src/ledger/database/interface.zig | 8 ++--- src/ledger/database/lmdb.zig | 4 +-- src/ledger/database/rocksdb.zig | 2 +- src/ledger/reader.zig | 36 ++++++++++---------- src/ledger/result_writer.zig | 14 ++++---- src/ledger/shred_inserter/shred_inserter.zig | 19 ++++++----- src/ledger/shred_inserter/working_state.zig | 2 +- 10 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/ledger/benchmarks.zig b/src/ledger/benchmarks.zig index 1791d2b70..0bc612dde 100644 --- a/src/ledger/benchmarks.zig +++ b/src/ledger/benchmarks.zig @@ -348,7 +348,7 @@ pub const BenchmarkLedgerSlow = struct { // connect the chain parent_slot = slot; } - try db.commit(write_batch); + try db.commit(&write_batch); var timer = try sig.time.Timer.start(); const is_connected = try reader.slotRangeConnected(1, slot_per_epoch); diff --git a/src/ledger/cleanup_service.zig b/src/ledger/cleanup_service.zig index e6fae23e1..d4b174e48 100644 --- a/src/ledger/cleanup_service.zig +++ b/src/ledger/cleanup_service.zig @@ -213,7 +213,7 @@ pub fn purgeSlots(db: *BlockstoreDB, from_slot: Slot, to_slot: Slot) !bool { writePurgeRange(&write_batch, from_slot, purge_to_slot) catch { did_purge = false; }; - try db.commit(write_batch); + try db.commit(&write_batch); if (did_purge and from_slot == 0) { try purgeFilesInRange(db, from_slot, purge_to_slot); @@ -373,7 +373,7 @@ test "findSlotsToClean" { defer write_batch.deinit(); try write_batch.put(ledger.schema.schema.slot_meta, lowest_slot_meta.slot, lowest_slot_meta); try write_batch.put(ledger.schema.schema.slot_meta, highest_slot_meta.slot, highest_slot_meta); - try db.commit(write_batch); + try db.commit(&write_batch); } const r = try findSlotsToClean(&reader, 0, 100); @@ -436,7 +436,7 @@ test "purgeSlots" { try write_batch.put(schema.merkle_root_meta, merkle_root_meta, merkle_meta); } - try db.commit(write_batch); + try db.commit(&write_batch); // purge the range [0, 5] const did_purge2 = try purgeSlots(&db, 0, 5); diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index 320725844..25e875d7f 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -165,7 +165,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { /// Atomicity may be violated if there is insufficient /// memory to complete a PUT. - pub fn commit(self: *Self, batch: WriteBatch) Allocator.Error!void { + pub fn commit(self: *Self, batch: *WriteBatch) Allocator.Error!void { self.transaction_lock.lock(); defer self.transaction_lock.unlock(); diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index 7d3f1073e..04edeedb1 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -98,8 +98,8 @@ pub fn Database(comptime Impl: type) type { return .{ .impl = try self.impl.initWriteBatch() }; } - pub fn commit(self: *Self, batch: WriteBatch) anyerror!void { - return self.impl.commit(batch.impl); + pub fn commit(self: *Self, batch: *WriteBatch) anyerror!void { + return self.impl.commit(&batch.impl); } /// A write batch is a sequence of operations that execute atomically. @@ -328,7 +328,7 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { try std.testing.expectEqual(null, try db.get(allocator, cf2, 321)); try std.testing.expectEqual(null, try db.get(allocator, cf2, 333)); - try db.commit(batch); + try db.commit(&batch); try std.testing.expectEqual(null, try db.get(allocator, cf1, 0)); try std.testing.expectEqual(Value1{ .hello = 100 }, try db.get(allocator, cf1, 123)); @@ -706,7 +706,7 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { var batch = try db.initWriteBatch(); defer batch.deinit(); try batch.deleteRange(cf1, 0, 100); - try db.commit(batch); + try db.commit(&batch); try std.testing.expectEqual(null, try db.get(allocator, cf1, 10)); try std.testing.expectEqual(null, try db.get(allocator, cf1, 20)); diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index e503005f7..73cdf7e85 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -163,7 +163,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var batch = try self.initWriteBatch(); errdefer batch.deinit(); try batch.deleteRange(cf, start, end); - try self.commit(batch); + try self.commit(&batch); } pub fn initWriteBatch(self: *Self) anyerror!WriteBatch { @@ -178,7 +178,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { }; } - pub fn commit(_: *Self, batch: WriteBatch) LmdbError!void { + pub fn commit(_: *Self, batch: *WriteBatch) LmdbError!void { try maybeError(c.mdb_txn_commit(batch.txn)); } diff --git a/src/ledger/database/rocksdb.zig b/src/ledger/database/rocksdb.zig index d77a3a23c..99a185f04 100644 --- a/src/ledger/database/rocksdb.zig +++ b/src/ledger/database/rocksdb.zig @@ -180,7 +180,7 @@ pub fn RocksDB(comptime column_families: []const ColumnFamily) type { }; } - pub fn commit(self: *Self, batch: WriteBatch) Error!void { + pub fn commit(self: *Self, batch: *WriteBatch) Error!void { return callRocks(self.logger, rocks.DB.write, .{ &self.db, batch.inner }); } diff --git a/src/ledger/reader.zig b/src/ledger/reader.zig index a56e28ba3..41e3b0d00 100644 --- a/src/ledger/reader.zig +++ b/src/ledger/reader.zig @@ -1547,7 +1547,7 @@ test "getLatestOptimisticSlots" { .timestamp = 10, }, }); - try db.commit(write_batch); + try db.commit(&write_batch); const get_hash, const ts = (try reader.getOptimisticSlot(1)).?; try std.testing.expectEqual(hash, get_hash); @@ -1572,7 +1572,7 @@ test "getLatestOptimisticSlots" { .timestamp = 100, }, }); - try db.commit(write_batch); + try db.commit(&write_batch); const get_hash, const ts = (try reader.getOptimisticSlot(10)).?; try std.testing.expectEqual(hash, get_hash); @@ -1618,7 +1618,7 @@ test "getFirstDuplicateProof" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.duplicate_slots, 19, proof); - try db.commit(write_batch); + try db.commit(&write_batch); const slot, const proof2 = (try reader.getFirstDuplicateProof()).?; defer bincode.free(allocator, proof2); @@ -1652,7 +1652,7 @@ test "isDead" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.dead_slots, 19, true); - try db.commit(write_batch); + try db.commit(&write_batch); } try std.testing.expectEqual(try reader.isDead(19), true); @@ -1660,7 +1660,7 @@ test "isDead" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.dead_slots, 19, false); - try db.commit(write_batch); + try db.commit(&write_batch); } try std.testing.expectEqual(try reader.isDead(19), false); } @@ -1687,7 +1687,7 @@ test "getBlockHeight" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.block_height, 19, 19); - try db.commit(write_batch); + try db.commit(&write_batch); // should succeeed const height = try reader.getBlockHeight(19); @@ -1716,7 +1716,7 @@ test "getRootedBlockTime" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.blocktime, 19, 19); - try db.commit(write_batch); + try db.commit(&write_batch); // not rooted const r = reader.getRootedBlockTime(19); @@ -1726,7 +1726,7 @@ test "getRootedBlockTime" { var write_batch2 = try db.initWriteBatch(); defer write_batch2.deinit(); try write_batch2.put(schema.rooted_slots, 19, true); - try db.commit(write_batch2); + try db.commit(&write_batch2); // should succeeed const time = try reader.getRootedBlockTime(19); @@ -1780,7 +1780,7 @@ test "slotMetaIterator" { try slot_metas.append(slot_meta); } - try db.commit(write_batch); + try db.commit(&write_batch); var iter = try reader.slotMetaIterator(0); defer iter.deinit(); @@ -1820,7 +1820,7 @@ test "rootedSlotIterator" { for (roots) |slot| { try write_batch.put(schema.rooted_slots, slot, true); } - try db.commit(write_batch); + try db.commit(&write_batch); var iter = try reader.rootedSlotIterator(0); defer iter.deinit(); @@ -1870,7 +1870,7 @@ test "slotRangeConnected" { // connect the chain parent_slot = slot; } - try db.commit(write_batch); + try db.commit(&write_batch); var write_batch2 = try db.initWriteBatch(); defer write_batch2.deinit(); @@ -1925,7 +1925,7 @@ test "highestSlot" { shred_slot, slot_meta, ); - try db.commit(write_batch); + try db.commit(&write_batch); const highest_slot = (try reader.highestSlot()).?; try std.testing.expectEqual(slot_meta.slot, highest_slot); @@ -1944,7 +1944,7 @@ test "highestSlot" { slot_meta2.slot, slot_meta2, ); - try db.commit(write_batch); + try db.commit(&write_batch); const highest_slot = (try reader.highestSlot()).?; try std.testing.expectEqual(slot_meta2.slot, highest_slot); @@ -1991,7 +1991,7 @@ test "lowestSlot" { shred_slot, slot_meta, ); - try db.commit(write_batch); + try db.commit(&write_batch); const lowest_slot = try reader.lowestSlot(); try std.testing.expectEqual(slot_meta.slot, lowest_slot); @@ -2040,7 +2040,7 @@ test "isShredDuplicate" { .{ shred_slot, shred_index }, shred_payload, ); - try db.commit(write_batch); + try db.commit(&write_batch); // should now be a duplicate const other_payload = (try reader.isShredDuplicate(shred)).?; @@ -2101,7 +2101,7 @@ test "findMissingDataIndexes" { shred_slot, slot_meta, ); - try db.commit(write_batch); + try db.commit(&write_batch); var indexes = try reader.findMissingDataIndexes( slot_meta.slot, @@ -2165,7 +2165,7 @@ test "getCodeShred" { .{ shred_slot, shred_index }, shred.payload(), ); - try db.commit(write_batch); + try db.commit(&write_batch); // correct data read const code_shred = try reader.getCodeShred(shred_slot, shred_index) orelse { @@ -2235,7 +2235,7 @@ test "getDataShred" { .{ shred_slot, shred_index }, shred_payload, ); - try db.commit(write_batch); + try db.commit(&write_batch); // correct data read const data_shred = try reader.getDataShred( diff --git a/src/ledger/result_writer.zig b/src/ledger/result_writer.zig index 850c5a02a..ff31b339b 100644 --- a/src/ledger/result_writer.zig +++ b/src/ledger/result_writer.zig @@ -120,7 +120,7 @@ pub const LedgerResultWriter = struct { } }; try write_batch.put(schema.bank_hash, slot, data); } - try self.db.commit(write_batch); + try self.db.commit(&write_batch); } /// agave: set_roots @@ -133,7 +133,7 @@ pub const LedgerResultWriter = struct { try write_batch.put(schema.rooted_slots, slot, true); } - try self.db.commit(write_batch); + try self.db.commit(&write_batch); _ = self.max_root.fetchMax(max_new_rooted_slot, .monotonic); } @@ -296,7 +296,7 @@ pub const LedgerResultWriter = struct { try write_batch.put(schema.slot_meta, slot_meta.slot, slot_meta); } - try self.db.commit(write_batch); + try self.db.commit(&write_batch); } fn isRoot(self: *Self, slot: Slot) !bool { @@ -375,7 +375,7 @@ test "scanAndFixRoots" { try write_batch.put(schema.slot_meta, slot_meta_1.slot, slot_meta_1); try write_batch.put(schema.slot_meta, slot_meta_2.slot, slot_meta_2); try write_batch.put(schema.slot_meta, slot_meta_3.slot, slot_meta_3); - try db.commit(write_batch); + try db.commit(&write_batch); const exit = std.atomic.Value(bool).init(false); @@ -411,7 +411,7 @@ test "setAndChainConnectedOnRootAndNextSlots" { var write_batch = try db.initWriteBatch(); defer write_batch.deinit(); try write_batch.put(schema.slot_meta, slot_meta_1.slot, slot_meta_1); - try db.commit(write_batch); + try db.commit(&write_batch); try std.testing.expectEqual(false, slot_meta_1.isConnected()); @@ -445,7 +445,7 @@ test "setAndChainConnectedOnRootAndNextSlots" { // connect the chain parent_slot = slot; } - try db.commit(write_batch2); + try db.commit(&write_batch2); try writer.setAndChainConnectedOnRootAndNextSlots(other_roots[0]); @@ -504,7 +504,7 @@ test "setAndChainConnectedOnRootAndNextSlots: disconnected" { slot_meta_3.consecutive_received_from_0 = 1 + 1; try write_batch.put(schema.slot_meta, slot_meta_3.slot, slot_meta_3); - try db.commit(write_batch); + try db.commit(&write_batch); try writer.setAndChainConnectedOnRootAndNextSlots(1); diff --git a/src/ledger/shred_inserter/shred_inserter.zig b/src/ledger/shred_inserter/shred_inserter.zig index f5b7d012d..4146c31bd 100644 --- a/src/ledger/shred_inserter/shred_inserter.zig +++ b/src/ledger/shred_inserter/shred_inserter.zig @@ -170,7 +170,7 @@ pub const ShredInserter = struct { self.metrics, ); defer state.deinit(); - var write_batch = state.write_batch; + const write_batch = &state.write_batch; const merkle_root_validator = MerkleRootValidator.init(&state); var get_lock_timer = try Timer.start(); @@ -192,7 +192,7 @@ pub const ShredInserter = struct { data_shred, &state, merkle_root_validator, - &write_batch, + write_batch, is_trusted, leader_schedule, shred_source, @@ -223,7 +223,7 @@ pub const ShredInserter = struct { code_shred, &state, merkle_root_validator, - &write_batch, + write_batch, is_trusted, shred_source, ); @@ -277,7 +277,7 @@ pub const ShredInserter = struct { shred.data, &state, merkle_root_validator, - &write_batch, + write_batch, is_trusted, leader_schedule, .recovered, @@ -311,7 +311,7 @@ pub const ShredInserter = struct { try handleChaining( allocator, &self.db, - &write_batch, + write_batch, &state.slot_meta_working_set, ); self.metrics.chaining_elapsed_us.add(chaining_timer.read().asMicros()); @@ -1217,7 +1217,8 @@ test "insertShreds single shred" { schema.data_shred, .{ shred.commonHeader().slot, shred.commonHeader().index }, ); - defer stored_shred.?.deinit(); + defer if (stored_shred) |s| s.deinit(); + if (stored_shred == null) return error.Fail; try std.testing.expectEqualSlices(u8, shred.payload(), stored_shred.?.data); } @@ -1371,7 +1372,7 @@ test "merkle root metas coding" { working_merkle_root_meta.asRef().*, ); } - try state.db.commit(write_batch); + try state.db.commit(&write_batch); } var insert_state = try PendingInsertShredsState.init( @@ -1416,7 +1417,7 @@ test "merkle root metas coding" { try std.testing.expectEqual(original_meta.first_received_shred_type, .code); } - try state.db.commit(write_batch); + try state.db.commit(&write_batch); } insert_state.duplicate_shreds.clearRetainingCapacity(); @@ -1458,7 +1459,7 @@ test "merkle root metas coding" { try std.testing.expectEqual(merkle_root_meta.first_received_shred_index, this_index); try std.testing.expectEqual(merkle_root_meta.first_received_shred_type, .code); - try state.db.commit(write_batch); + try state.db.commit(&write_batch); } } diff --git a/src/ledger/shred_inserter/working_state.zig b/src/ledger/shred_inserter/working_state.zig index 46dad1b10..00d38444a 100644 --- a/src/ledger/shred_inserter/working_state.zig +++ b/src/ledger/shred_inserter/working_state.zig @@ -217,7 +217,7 @@ pub const PendingInsertShredsState = struct { self.metrics.insert_working_sets_elapsed_us.add(commit_working_sets_timer.read().asMicros()); var commit_timer = try Timer.start(); - try self.db.commit(self.write_batch); + try self.db.commit(&self.write_batch); self.metrics.write_batch_elapsed_us.add(commit_timer.read().asMicros()); } From 4710c138eaaa594ab73da4465b9f6a8813f0dd5a Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 18:51:47 -0500 Subject: [PATCH 26/32] fix(ledger): memory leaks in tests --- src/ledger/shred_inserter/shred_inserter.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ledger/shred_inserter/shred_inserter.zig b/src/ledger/shred_inserter/shred_inserter.zig index 4146c31bd..e66e7a73a 100644 --- a/src/ledger/shred_inserter/shred_inserter.zig +++ b/src/ledger/shred_inserter/shred_inserter.zig @@ -1242,6 +1242,7 @@ test "insertShreds 100 shreds from mainnet" { schema.data_shred, .{ shred.commonHeader().slot, shred.commonHeader().index }, ); + defer if (bytes) |b| b.deinit(); try std.testing.expectEqualSlices(u8, shred.payload(), bytes.?.data); } } @@ -1420,6 +1421,12 @@ test "merkle root metas coding" { try state.db.commit(&write_batch); } + for (insert_state.duplicate_shreds.items) |duplicate| { + switch (duplicate) { + .Exists => |s| s.deinit(), + inline else => |sc| insert_state.allocator.free(sc.conflict), + } + } insert_state.duplicate_shreds.clearRetainingCapacity(); { // third shred (different index, should succeed) From 25bd0994e12c01cd0f056fddab7790f292a04725 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Tue, 5 Nov 2024 19:00:23 -0500 Subject: [PATCH 27/32] ci: explicitly test ledger databases in github workflow, and don't require all dependencies during partial test runs --- .github/workflows/check.yml | 5 ++++- build.zig | 7 +++++-- src/ledger/database/hashmap.zig | 5 ++++- src/ledger/database/lmdb.zig | 5 ++++- src/ledger/database/rocksdb.zig | 5 ++++- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a46208baf..55d31c18c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -54,7 +54,10 @@ jobs: version: 0.13.0 - name: test - run: zig build test -Denable-tsan=true + run: | + zig build test -Denable-tsan=true + zig build test -Denable-tsan=true -Dblockstore-db=lmdb -Dfilter=ledger + zig build test -Denable-tsan=true -Dblockstore-db=hashmap -Dfilter=ledger kcov_test: strategy: diff --git a/build.zig b/build.zig index b99cb0cab..15d058a7d 100644 --- a/build.zig +++ b/build.zig @@ -128,8 +128,11 @@ pub fn build(b: *Build) void { unit_tests_exe.root_module.addImport("httpz", httpz_mod); unit_tests_exe.root_module.addImport("zig-network", zig_network_module); unit_tests_exe.root_module.addImport("zstd", zstd_mod); - unit_tests_exe.root_module.addImport("rocksdb", rocksdb_mod); - unit_tests_exe.root_module.addImport("lmdb", lmdb_mod); + switch (blockstore_db) { + .rocksdb => unit_tests_exe.root_module.addImport("rocksdb", rocksdb_mod), + .lmdb => unit_tests_exe.root_module.addImport("lmdb", lmdb_mod), + .hashmap => {}, + } unit_tests_exe.root_module.addOptions("build-options", build_options); unit_tests_exe.linkLibC(); diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index 25e875d7f..fea72e39e 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -1,6 +1,7 @@ const std = @import("std"); const sig = @import("../../sig.zig"); const database = @import("lib.zig"); +const build_options = @import("build-options"); const Allocator = std.mem.Allocator; const DefaultRwLock = std.Thread.RwLock.DefaultRwLock; @@ -454,5 +455,7 @@ const SharedHashMap = struct { }; comptime { - _ = &database.interface.testDatabase(SharedHashMapDB); + if (build_options.blockstore_db == .hashmap) { + _ = &database.interface.testDatabase(SharedHashMapDB); + } } diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 73cdf7e85..c991fb997 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -2,6 +2,7 @@ const std = @import("std"); const c = @import("lmdb"); const sig = @import("../../sig.zig"); const database = @import("lib.zig"); +const build_options = @import("build-options"); const Allocator = std.mem.Allocator; @@ -634,5 +635,7 @@ pub const LmdbError = sig.utils.errors.LibcError || error{ }; comptime { - _ = &database.interface.testDatabase(LMDB); + if (build_options.blockstore_db == .lmdb) { + _ = &database.interface.testDatabase(LMDB); + } } diff --git a/src/ledger/database/rocksdb.zig b/src/ledger/database/rocksdb.zig index 99a185f04..2f8827f32 100644 --- a/src/ledger/database/rocksdb.zig +++ b/src/ledger/database/rocksdb.zig @@ -2,6 +2,7 @@ const std = @import("std"); const rocks = @import("rocksdb"); const sig = @import("../../sig.zig"); const database = @import("lib.zig"); +const build_options = @import("build-options"); const Allocator = std.mem.Allocator; @@ -341,5 +342,7 @@ fn callRocks(logger: Logger, comptime func: anytype, args: anytype) ReturnType(@ } comptime { - _ = &database.interface.testDatabase(RocksDB); + if (build_options.blockstore_db == .rocksdb) { + _ = &database.interface.testDatabase(RocksDB); + } } From ac6ba87f6d75000faf6a31a2ccc3c650e2c9a0c4 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 6 Nov 2024 09:36:31 -0500 Subject: [PATCH 28/32] fix(ledger): memory bugs in the shred inserter --- src/ledger/shred_inserter/merkle_root_checks.zig | 7 +++++-- src/ledger/shred_inserter/shred_inserter.zig | 14 ++++++++------ src/ledger/shred_inserter/working_state.zig | 14 +++++++------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/ledger/shred_inserter/merkle_root_checks.zig b/src/ledger/shred_inserter/merkle_root_checks.zig index 2ac2a4e1b..1047af9c7 100644 --- a/src/ledger/shred_inserter/merkle_root_checks.zig +++ b/src/ledger/shred_inserter/merkle_root_checks.zig @@ -210,10 +210,11 @@ pub const MerkleRootValidator = struct { ); return true; }; + errdefer other_shred.deinit(); const older_shred, const newer_shred = switch (direction) { - .forward => .{ shred.payload(), other_shred }, - .backward => .{ other_shred, shred.payload() }, + .forward => .{ shred.payload(), other_shred.data }, + .backward => .{ other_shred.data, shred.payload() }, }; const chained_merkle_root = shred_mod.layout.getChainedMerkleRoot(newer_shred); @@ -252,6 +253,8 @@ pub const MerkleRootValidator = struct { try self.duplicate_shreds.append(.{ .ChainedMerkleRootConflict = .{ .original = shred, .conflict = other_shred }, }); + } else { + other_shred.deinit(); } return false; diff --git a/src/ledger/shred_inserter/shred_inserter.zig b/src/ledger/shred_inserter/shred_inserter.zig index e66e7a73a..19d7854f3 100644 --- a/src/ledger/shred_inserter/shred_inserter.zig +++ b/src/ledger/shred_inserter/shred_inserter.zig @@ -29,6 +29,7 @@ const SortedMap = sig.utils.collections.SortedMap; const Timer = sig.time.Timer; const BlockstoreDB = ledger.blockstore.BlockstoreDB; +const BytesRef = ledger.database.BytesRef; const IndexMetaWorkingSetEntry = lib.working_state.IndexMetaWorkingSetEntry; const MerkleRootValidator = lib.merkle_root_checks.MerkleRootValidator; const PendingInsertShredsState = lib.working_state.PendingInsertShredsState; @@ -382,6 +383,7 @@ pub const ShredInserter = struct { return .{ .completed_data_set_infos = newly_completed_data_sets, + // TODO: ensure all duplicate shreds exceed lifetime of pending state .duplicate_shreds = state.duplicate_shreds, }; } @@ -521,7 +523,7 @@ pub const ShredInserter = struct { )) |conflicting_shred| { // found the duplicate self.db.put(schema.duplicate_slots, slot, .{ - .shred1 = conflicting_shred, + .shred1 = conflicting_shred.data, .shred2 = shred.payload, }) catch |e| { // TODO: only log a database error? @@ -566,7 +568,7 @@ pub const ShredInserter = struct { _: CodeShred, // TODO: figure out why this is here. delete it or add what is missing. slot: Slot, erasure_meta: *const ErasureMeta, - ) !?[]const u8 { // TODO consider lifetime + ) !?BytesRef { // TODO consider lifetime // Search for the shred which set the initial erasure config, either inserted, // or in the current batch in just_inserted_shreds. const index: u32 = @intCast(erasure_meta.first_received_code_index); @@ -728,7 +730,6 @@ pub const ShredInserter = struct { .index = shred_index_u32, .shred_type = .data, }; - // FIXME: leak - decide how to free shred const maybe_shred = try shred_store.get(shred_id); const ending_shred = if (maybe_shred) |s| s else { self.logger.err().logf(&newlinesToSpaces( @@ -739,8 +740,9 @@ pub const ShredInserter = struct { ), .{ shred_id, slot_meta }); return false; // TODO: this is redundant }; + errdefer ending_shred.deinit(); const dupe = meta.DuplicateSlotProof{ - .shred1 = ending_shred, + .shred1 = ending_shred.data, .shred2 = shred.payload, }; self.db.put(schema.duplicate_slots, slot, dupe) catch |e| { @@ -1423,8 +1425,8 @@ test "merkle root metas coding" { for (insert_state.duplicate_shreds.items) |duplicate| { switch (duplicate) { - .Exists => |s| s.deinit(), - inline else => |sc| insert_state.allocator.free(sc.conflict), + .Exists => {}, + inline else => |sc| sc.conflict.deinit(), } } insert_state.duplicate_shreds.clearRetainingCapacity(); diff --git a/src/ledger/shred_inserter/working_state.zig b/src/ledger/shred_inserter/working_state.zig index 00d38444a..523d08ac3 100644 --- a/src/ledger/shred_inserter/working_state.zig +++ b/src/ledger/shred_inserter/working_state.zig @@ -16,6 +16,7 @@ const Timer = sig.time.Timer; const BlockstoreDB = ledger.blockstore.BlockstoreDB; const BlockstoreInsertionMetrics = shred_inserter.shred_inserter.BlockstoreInsertionMetrics; +const BytesRef = ledger.database.BytesRef; const CodeShred = ledger.shred.CodeShred; const ColumnFamily = ledger.database.ColumnFamily; const ErasureSetId = ledger.shred.ErasureSetId; @@ -485,7 +486,7 @@ pub const PossibleDuplicateShred = union(enum) { const ShredConflict = struct { original: Shred, - conflict: []const u8, + conflict: BytesRef, }; pub const ShredWorkingStore = struct { @@ -495,13 +496,12 @@ pub const ShredWorkingStore = struct { const Self = @This(); - // TODO consider lifetime -> return must inform a conditional deinit - pub fn get(self: Self, id: ShredId) !?[]const u8 { + /// returned shred lifetime does not exceed this struct + pub fn get(self: Self, id: ShredId) !?BytesRef { if (self.just_inserted_shreds.get(id)) |shred| { - return shred.payload(); // owned by map + return .{ .data = shred.payload(), .allocator = null }; } return switch (id.shred_type) { - // owned by database .data => self.getFromDb(schema.data_shred, id), .code => self.getFromDb(schema.code_shred, id), }; @@ -539,9 +539,9 @@ pub const ShredWorkingStore = struct { } else null; } - fn getFromDb(self: Self, comptime cf: ColumnFamily, id: ShredId) !?[]const u8 { + fn getFromDb(self: Self, comptime cf: ColumnFamily, id: ShredId) !?BytesRef { return if (try self.db.getBytes(cf, .{ id.slot, @intCast(id.index) })) |s| - s.data + s else null; } From 385aa538fabb237f3c13a861d1a7c9fcecf76e0b Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 6 Nov 2024 10:21:23 -0500 Subject: [PATCH 29/32] test(ledger): improve deleteRange test --- src/ledger/database/interface.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index 04edeedb1..e5955b7b0 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -705,13 +705,13 @@ pub fn testDatabase(comptime Impl: fn ([]const ColumnFamily) type) type { var batch = try db.initWriteBatch(); defer batch.deinit(); - try batch.deleteRange(cf1, 0, 100); + try batch.deleteRange(cf1, 15, 35); try db.commit(&batch); - try std.testing.expectEqual(null, try db.get(allocator, cf1, 10)); + try std.testing.expect(null != try db.get(allocator, cf1, 10)); try std.testing.expectEqual(null, try db.get(allocator, cf1, 20)); try std.testing.expectEqual(null, try db.get(allocator, cf1, 30)); - try std.testing.expectEqual(null, try db.get(allocator, cf1, 40)); + try std.testing.expect(null != try db.get(allocator, cf1, 40)); } }; } From fd148061203dc0d07676fedbbf2970abe046ee96 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 6 Nov 2024 11:17:38 -0500 Subject: [PATCH 30/32] fix(ledger): close lmdb env on deinit, and don't abort write txns There is a bizarre behavior of lmdb that is not documented anywhere. LMDB always reuses the same transaction state for every write transaction. A pointer to it is stored in the env struct and recycled. Aborting a transaction frees the transaction pointer. So if you abort a write transaction, it frees the only write transaction pointer. This corrupts the memory of lmdb because it will try to use the same pointer later as if it is valid. I can't understand how this behavior of lmdb is in any way sane or reasonable, so maybe I'm missing something. Anyway, when you close the env, it tries to free the write transaction, leading to a double free if you already aborted the transaction. that's why it cropped up during this change. so I'm just having it reset write transactions now, instead of abort, which should be fine --- src/ledger/database/lmdb.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index c991fb997..870f704b5 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -34,7 +34,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { // begin transaction to create column families aka "databases" in lmdb const txn = try returnOutput(c.mdb_txn_begin, .{ env, null, 0 }); - errdefer c.mdb_txn_abort(txn); + errdefer c.mdb_txn_reset(txn); // allocate cf handles const dbis = try allocator.alloc(c.MDB_dbi, column_families.len); @@ -60,6 +60,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn deinit(self: *Self) void { self.allocator.free(self.dbis); self.allocator.free(self.path); + c.mdb_env_close(self.env); } fn dbi(self: *Self, comptime cf: ColumnFamily) c.MDB_dbi { @@ -87,7 +88,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { defer val_bytes.deinit(); const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_abort(txn); + errdefer c.mdb_txn_reset(txn); var key_val = toVal(key_bytes.data); var val_val = toVal(val_bytes.data); @@ -146,7 +147,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { var val_val: c.MDB_val = undefined; const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); - errdefer c.mdb_txn_abort(txn); + errdefer c.mdb_txn_reset(txn); maybeError(c.mdb_del(txn, self.dbi(cf), &key_val, &val_val)) catch |e| switch (e) { error.MDB_NOTFOUND => {}, @@ -200,7 +201,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { executed: *bool, pub fn deinit(self: *WriteBatch) void { - if (!self.executed.*) c.mdb_txn_abort(self.txn); + if (!self.executed.*) c.mdb_txn_reset(self.txn); self.allocator.destroy(self.executed); } @@ -274,7 +275,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { null; defer if (maybe_start_bytes) |sb| sb.deinit(); - const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, 0 }); + const txn = try returnOutput(c.mdb_txn_begin, .{ self.env, null, c.MDB_RDONLY }); errdefer c.mdb_txn_abort(txn); const cursor = try returnOutput(c.mdb_cursor_open, .{ txn, self.dbi(cf) }); From 42e760891c641f3c55085a91a3ddd85d46c3bd86 Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 6 Nov 2024 11:27:54 -0500 Subject: [PATCH 31/32] test(ledger): run testDatabase(hashmap) tests no matter what --- src/ledger/database/hashmap.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index fea72e39e..69ae308ca 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -455,7 +455,5 @@ const SharedHashMap = struct { }; comptime { - if (build_options.blockstore_db == .hashmap) { - _ = &database.interface.testDatabase(SharedHashMapDB); - } + _ = &database.interface.testDatabase(SharedHashMapDB); } From 53a9ea9a0d0407a054e145d7628fd2fa03d4209c Mon Sep 17 00:00:00 2001 From: Drew Nutter Date: Wed, 6 Nov 2024 13:35:17 -0500 Subject: [PATCH 32/32] fix(ledger): allocator misuse in BytesRef allocator was misused for generic recycling of resources. this broke with lmdb because it segfaults when Allocator.free attempts to overwrite the with `undefined` --- src/ledger/database/hashmap.zig | 6 +-- src/ledger/database/interface.zig | 41 +++++++++++++++++++-- src/ledger/database/lmdb.zig | 29 +++++---------- src/ledger/database/rocksdb.zig | 6 +-- src/ledger/shred_inserter/working_state.zig | 2 +- 5 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/ledger/database/hashmap.zig b/src/ledger/database/hashmap.zig index 69ae308ca..a6230abd3 100644 --- a/src/ledger/database/hashmap.zig +++ b/src/ledger/database/hashmap.zig @@ -113,7 +113,7 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { const ret = try self.allocator.alloc(u8, val_bytes.len); @memcpy(ret, val_bytes); return .{ - .allocator = self.allocator, + .deinitializer = BytesRef.Deinitializer.fromAllocator(self.allocator), .data = ret, }; } @@ -375,8 +375,8 @@ pub fn SharedHashMapDB(comptime column_families: []const ColumnFamily) type { pub fn nextBytes(self: *@This()) error{}!?[2]BytesRef { const index = self.nextIndex() orelse return null; return .{ - .{ .allocator = null, .data = self.keys[index] }, - .{ .allocator = null, .data = self.vals[index] }, + .{ .deinitializer = null, .data = self.keys[index] }, + .{ .deinitializer = null, .data = self.vals[index] }, }; } diff --git a/src/ledger/database/interface.zig b/src/ledger/database/interface.zig index e5955b7b0..407ef37fa 100644 --- a/src/ledger/database/interface.zig +++ b/src/ledger/database/interface.zig @@ -224,10 +224,10 @@ fn serializer(endian: std.builtin.Endian) type { /// Use this if the database backend accepts a pointer and immediately calls memcpy. pub fn serializeToRef(allocator: Allocator, item: anytype) !BytesRef { return if (@TypeOf(item) == []const u8 or @TypeOf(item) == []u8) .{ - .allocator = null, + .deinitializer = null, .data = item, } else .{ - .allocator = allocator, + .deinitializer = BytesRef.Deinitializer.fromAllocator(allocator), .data = try serializeAlloc(allocator, item), }; } @@ -249,12 +249,45 @@ fn serializer(endian: std.builtin.Endian) type { } pub const BytesRef = struct { - allocator: ?Allocator = null, + deinitializer: ?Deinitializer = null, data: []const u8, pub fn deinit(self: @This()) void { - if (self.allocator) |a| a.free(self.data); + if (self.deinitializer) |d| d.deinit(self.data); } + + pub const Deinitializer = union(enum) { + allocator: Allocator, + generic: struct { + context: *anyopaque, + deinit: *const fn (*anyopaque, []const u8) void, + }, + + pub fn init( + context: anytype, + deinitFn: fn (@TypeOf(context), []const u8) void, + ) Deinitializer { + return .{ .generic = .{ + .context = @alignCast(@ptrCast(context)), + .deinit = &struct { + fn genericDeinit(ctx: *anyopaque, data: []const u8) void { + deinitFn(@alignCast(@ptrCast(ctx)), data); + } + }.genericDeinit, + } }; + } + + pub fn fromAllocator(allocator: Allocator) Deinitializer { + return .{ .allocator = allocator }; + } + + pub fn deinit(self: Deinitializer, data: []const u8) void { + switch (self) { + .generic => |generic| generic.deinit(generic.context, data), + .allocator => |allocator| allocator.free(data), + } + } + }; }; /// Test cases that can be applied to any implementation of Database diff --git a/src/ledger/database/lmdb.zig b/src/ledger/database/lmdb.zig index 870f704b5..ba602e80f 100644 --- a/src/ledger/database/lmdb.zig +++ b/src/ledger/database/lmdb.zig @@ -131,7 +131,7 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { }; return .{ - .allocator = txnAborter(txn), + .deinitializer = txnAborter(txn), .data = fromVal(item), }; } @@ -348,8 +348,8 @@ pub fn LMDB(comptime column_families: []const ColumnFamily) type { pub fn nextBytes(self: *@This()) LmdbError!?[2]BytesRef { const key, const val = try self.nextImpl() orelse return null; return .{ - .{ .allocator = null, .data = key }, - .{ .allocator = null, .data = val }, + .{ .deinitializer = null, .data = key }, + .{ .deinitializer = null, .data = val }, }; } @@ -374,29 +374,18 @@ fn fromVal(value: c.MDB_val) []const u8 { return ptr[0..value.mv_size]; } -/// Returns an `Allocator` that frees memory by aborting the transaction -/// that owns the memory. It cannot allocate anything. +/// Returns an `BytesRef.Deinitializer` that frees memory by aborting the transaction +/// that owns the memory. /// -/// This exists to be the Allocator used in a `BytesRef` instance -/// -/// Calling `free` with any input will free all memory that was allocated +/// Calling `deinit` with any input will free all memory that was allocated /// by the transaction. This means you cannot manage lifetimes of multiple /// items separately. Ideally you would only use this when you've only /// read exactly one item in the transaction. -fn txnAborter(txn: *c.MDB_txn) Allocator { - const vtable = .{ - .alloc = &sig.utils.allocators.noAlloc, - .resize = &Allocator.noResize, - .free = &resetTxnFree, - }; - return .{ - .ptr = @ptrCast(@alignCast(txn)), - .vtable = &vtable, - }; +fn txnAborter(txn: *c.MDB_txn) BytesRef.Deinitializer { + return BytesRef.Deinitializer.init(txn, resetTxnFree); } -fn resetTxnFree(ctx: *anyopaque, _: []u8, _: u8, _: usize) void { - const txn: *c.MDB_txn = @ptrCast(@alignCast(ctx)); +fn resetTxnFree(txn: *c.MDB_txn, _: []const u8) void { c.mdb_txn_abort(txn); } diff --git a/src/ledger/database/rocksdb.zig b/src/ledger/database/rocksdb.zig index 2f8827f32..002afc487 100644 --- a/src/ledger/database/rocksdb.zig +++ b/src/ledger/database/rocksdb.zig @@ -135,7 +135,7 @@ pub fn RocksDB(comptime column_families: []const ColumnFamily) type { .{ &self.db, self.cf_handles[cf.find(column_families)], key_bytes.data }, ) orelse return null; return .{ - .allocator = val_bytes.allocator, + .deinitializer = BytesRef.Deinitializer.fromAllocator(val_bytes.allocator), .data = val_bytes.data, }; } @@ -314,8 +314,8 @@ pub fn RocksDB(comptime column_families: []const ColumnFamily) type { pub fn nextBytes(self: *@This()) Error!?[2]BytesRef { const entry = try callRocks(self.logger, rocks.Iterator.next, .{&self.inner}); return if (entry) |kv| .{ - .{ .allocator = null, .data = kv[0].data }, - .{ .allocator = null, .data = kv[1].data }, + .{ .deinitializer = null, .data = kv[0].data }, + .{ .deinitializer = null, .data = kv[1].data }, } else null; } }; diff --git a/src/ledger/shred_inserter/working_state.zig b/src/ledger/shred_inserter/working_state.zig index 523d08ac3..16e356817 100644 --- a/src/ledger/shred_inserter/working_state.zig +++ b/src/ledger/shred_inserter/working_state.zig @@ -499,7 +499,7 @@ pub const ShredWorkingStore = struct { /// returned shred lifetime does not exceed this struct pub fn get(self: Self, id: ShredId) !?BytesRef { if (self.just_inserted_shreds.get(id)) |shred| { - return .{ .data = shred.payload(), .allocator = null }; + return .{ .data = shred.payload(), .deinitializer = null }; } return switch (id.shred_type) { .data => self.getFromDb(schema.data_shred, id),