-
Notifications
You must be signed in to change notification settings - Fork 34
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.
src/ledger/database/lmdb.zig
Outdated
c.mdb_txn_abort(txn); | ||
} | ||
|
||
fn ret(constructor: anytype, args: anytype) LmdbError!TypeToCreate(constructor) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/ledger/database/lmdb.zig
Outdated
// open cf/database, creating if necessary | ||
dbis[i] = try ret(c.mdb_dbi_open, .{ | ||
txn, | ||
@as([*c]const u8, @ptrCast(cf.name)), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things here.
- 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 callallocator.dupeZ
on the name, or just storecf.name
as a[:0]const u8
. - There is no need for a
@ptrCast
here, doingslice.ptr
yields[*]T
.
EDIT: lol, Sebastian has pointed out these exact same points in his review.
There was a problem hiding this comment.
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)), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
- 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.
- it's writing to the data somehow. writing to data in rodata is also invalid and would cause a fault.
There was a problem hiding this comment.
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
Outdated
prev_multiple = 18, | ||
}; | ||
|
||
fn result(int: isize) LmdbError!void { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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)), |
There was a problem hiding this comment.
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).
const key_serializer = database.interface.key_serializer; | ||
const value_serializer = database.interface.value_serializer; | ||
|
||
pub fn LMDB(comptime column_families: []const ColumnFamily) type { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Note that the "check_style" step that isn't "passing", is because #360 needs to be merged first, and this branch rebased onto it. |
…d leads to memory leaks
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`
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:
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.