Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ledger): support LMDB as a blockstore database backend #352

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

dnut
Copy link
Contributor

@dnut dnut commented Nov 1, 2024

https://github.com/Syndica/lmdb-zig

The bulk of this PR is an implementation for the blockstore's Database interface using LMDB, and adding more tests for the interface.

I also added a build option to customize the database backend, for example:

zig build -Dblockstore-db=lmdb

RocksDB remains the default for now.

For any build, only the necessary dependencies will be compiled. For example, lmdb will never be compiled, unless you specify -Dblockstore-db=lmdb.

A few bugs were uncovered in the hashmap db when testing the databases more strictly, so I fixed those in here as well.

Copy link
Contributor

@Rexicon226 Rexicon226 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just gone through it with a glance, I haven't looked into it too deep. I will return to my laptop and do a second review pass.

build.zig Outdated Show resolved Hide resolved
src/ledger/blockstore.zig Outdated Show resolved Hide resolved
src/ledger/database/lmdb.zig Show resolved Hide resolved
c.mdb_txn_abort(txn);
}

fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, and the functions under it are clearly relying on some behavior that makes no sense to me, a casual reader.
Could you document them better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// open cf/database, creating if necessary
dbis[i] = try ret(c.mdb_dbi_open, .{
txn,
@as([*c]const u8, @ptrCast(cf.name)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a translated file, it should never use C pointers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@Rexicon226 Rexicon226 Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here.

  1. I took a quick glance at lmbd, and it calls strlen on the name passed in. This should be a [*:0]const u8 type. Either call allocator.dupeZ on the name, or just store cf.name as a [:0]const u8.
  2. There is no need for a @ptrCast here, doing slice.ptr yields [*]T.

EDIT: lol, Sebastian has pointed out these exact same points in his review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn toVal(bytes: []const u8) c.MDB_val {
return .{
.mv_size = bytes.len,
.mv_data = @constCast(@ptrCast(bytes.ptr)),
Copy link
Contributor

@Rexicon226 Rexicon226 Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dunno about this. While I see what you're doing with this constCast, it can lead to very illegal behavior. One of the only use cases of constCast is to mitigate incorrect APIs, which I actually don't think this is.
I bet the implementation stores a mutable pointer because it frees it at some point. I have not looked into lmdb, so don't quote me on that.

Please take a []u8 here and duplicate constant strings or string literals instead. I refuse to use constCast anywhere in this codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please take a []u8 here and duplicate and constant strings or string literals instead.

This seems doable. I probably won't need to dupe anything. It will require some changes to preexisting code, but shouldn't be too bad.


I bet the implementation stores a mutable pointer because it frees it at some point.

toVal is used for inputs to LMDB. These are pointers that are allocated by the caller (in zig) and passed into an LMDB function as an input. They are not freed by LMDB.

One of the only use cases of constCast is to mitigate incorrect APIs, which I actually don't think this is.

My understanding is that when MDB_val is passed as an input into LMDB, the data behind the pointer is never mutated. It only reads the data.

Would you call that an incorrect API? If it says it takes mutable pointers but never mutates them?

it can lead to very illegal behavior

What kind of illegal behavior could happen in this situation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah lmdb's api is a bit odd w.r.t parameters there. As far as I understand it, yes, it will not modify that value at most call sites. The reason for this is some functions & options will use the key as an out param, e.g. mdb_cursor_get with MDB_GET_CURRENT will write to both the key and value params.

If you want to assert that it's const, I think it's best to be super explicit at the callsite of toVal. We wouldn't want to e.g. assert that something's const when it isn't (even if the memory is safely writeable).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weighing in:

Would you call that an incorrect API? If it says it takes mutable pointers but never mutates them?

Yeah, this is explicitly an incorrect API; it doesn't adequately communicate its intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining everyone, I agree lmdb is in the wrong here. Nevertheless, we should not use @constCast if we can avoid it.

What kind of illegal behavior could happen in this situation?

I originally assumed that either

  1. lmdb is freeing the data at some point, since free takes a mutable pointer. freeing a piece of data in a rodata section is invalid and would result in a panic.
  2. it's writing to the data somehow. writing to data in rodata is also invalid and would cause a fault.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this to require a mutable pointer without adding any extra allocations is doable but awkward. It requires some extra complexity to distinguish between input types, which would need to be mutable, and output types, which should probably be immutable.

It's strange to require callers to always pass mutable pointers into the database, even though it will never be mutated. It might encourage callers to allocate dupes before passing it in, which would be pointless.

src/ledger/database/lmdb.zig Show resolved Hide resolved
src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
prev_multiple = 18,
};

fn result(int: isize) LmdbError!void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think result is the best name for this.

These seems more like a "convert errno to error set" function, so I'd recommend it returns an error set and then be wrapped in a "check for error return code" function if that makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to find a better name for this, but I don't see the point of splitting it into two functions.

Assuming the name is descriptive, I feel that it helps readability for the name to be concise, since it's used for every call to lmdb. I'm struggling to find a name that feels satisfying in that regard. I think "lmdbReturnCodeToErrorUnion" is sufficiently descriptive, but it feels like clutter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about maybeError, which reads nicely in try maybeError(int), ie "try unwrap this which is maybe an error".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@Sobeston Sobeston left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broad strokes seems good, got a few comments for now

fn toVal(bytes: []const u8) c.MDB_val {
return .{
.mv_size = bytes.len,
.mv_data = @constCast(@ptrCast(bytes.ptr)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah lmdb's api is a bit odd w.r.t parameters there. As far as I understand it, yes, it will not modify that value at most call sites. The reason for this is some functions & options will use the key as an out param, e.g. mdb_cursor_get with MDB_GET_CURRENT will write to both the key and value params.

If you want to assert that it's const, I think it's best to be super explicit at the callsite of toVal. We wouldn't want to e.g. assert that something's const when it isn't (even if the memory is safely writeable).

src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
src/ledger/database/lmdb.zig Show resolved Hide resolved
src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
src/ledger/database/lmdb.zig Outdated Show resolved Hide resolved
const key_serializer = database.interface.key_serializer;
const value_serializer = database.interface.value_serializer;

pub fn LMDB(comptime column_families: []const ColumnFamily) type {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RocksDB implementation has the logger as part of the struct's states. Any reason not to have same for the LMDB?

Also, the logger can be scoped.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason rocksdb gets a logger is to communicate its errors through strings. Any time it returns an error, it also logs an error message, so the cause of the error is clear. LMDB only communicates errors through various error codes, which can be represented with zig errors.

}

pub fn deinit(self: *Self) void {
self.allocator.free(self.dbis);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of a question. There is also self.env, which is a pointer. What takes care of freeing that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it should be closed here.

Adding this also revealed another bug, which I fixed in the same commit. Oddly, aborting write transactions seems to lead to memory corruption, which is detected as a double free when closing the env.

2b7c2da

@Rexicon226
Copy link
Contributor

Rexicon226 commented Nov 6, 2024

Note that the "check_style" step that isn't "passing", is because #360 needs to be merged first, and this branch rebased onto it.
I changed the CI step name, and Github's branch protection rules work on specific names.

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
…quire all dependencies during partial test runs
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
allocator was misused for generic recycling of resources. this broke with lmdb because it segfaults when Allocator.free attempts to overwrite the with `undefined`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 👀 In review
Development

Successfully merging this pull request may close these issues.

5 participants