Bolt: A low-level key/value database for Go
github.com
github.com
If you're interested in working on projects like this, we are hiring - contact me tom at shopify.com.
https://github.com/boltdb/bolt#lmdb
Bolt was originally just a side project but we found that it fit our needs for our analytics database so we eventually swapped out LMDB for Bolt.
The way this is worded, it sounds like an improvement, but in reality bolt is no safer to use than LMDB was, it just lacks some configurable tradeoffs that were present in the original engine, and were safer to use than those that remain in bolt.
That said, I'm happy to see APPEND go ;)
I try to point out in the docs that DB.NoSync should only be used for offline bulk loading where corruption is typically less of an issue (since you can simply rebuild).
https://godoc.org/github.com/boltdb/bolt#DB
The wording was intended to make it sound like an improvement for people wanting smaller, more concise code bases. Bolt is 2KLOC vs LMDB's 8KLOC. Smaller surface area makes it easier to test thoroughly. It's definitely a tradeoff for people who want that really low level control though.
Note that APPEND mode won't allow you to corrupt the DB, despite what the doc says - you'll get a KEYEXIST error if you try to load out of order. We just left the doc unchanged to emphasize the importance of using sorted input.
I'd honestly suggest reading through the source code of some database projects. That's the best way to learn the structures and understand the engineering tradeoffs. Some are more approachable than others though. Bolt and LMDB are 2KLOC and 8KLOC, respectively. LevelDB is 20KLOC and RocksDB is 100KLOC, IIRC.
If you want to go further then I'd suggest porting one of those over to a language you know well. It doesn't have to be code you release to the world or use in production but just porting the code really helps to cement the concepts in my head.
To understand why the LMDB implementation does what it does will require much more practical knowledge, not theory. The references in my MDB papers (and the paper itself of course) will give you more of that. http://symas.com/mdb/#pubs
B+trees are trivially simple, in theory. Writing one that is actually efficient, safe, and supports high concurrency requires more than theory. It requires understanding of computer system architecture: CPU architecture/cache hierarchies, filesystem and I/O architecture, memory, etc.
It does not require 100KLOC though. RocksDB is just a pile of gratuitous complexity.
Pretty much Javascript callback spaghetti in Go. There might be a way around it, but I haven't found any examples. Perhaps there could be a way where transactions a manually managed.
I have observed before that this sort of DSL-like usage can sometimes make people forget that they are dealing with plain old-fashioned language constructs that can be manipulated with other constructs. Languages like Ruby that allow this fact to disappear out of the source code completely seem to make this more likely. Go won't let you forget you're just creating a bunch of closures, but if you've been trained in an enviroment where a "block" is something special or in such a Ruby "DSL" you may have a way of thinking about these issues that inhibits proper refactoring. This is the general case of what okatsu observes before me, that nothing stops you from making real functions or methods to use as the methods.
Also, Go's relatively recent "bound methods" are really useful for this sort of thing.
There are several benefits to using the wrapper functions:
1. It lets you return an error to rollback a read-write transaction. This is typically what you want. That error will get passed through as a return from DB.Update().
2. Because of #1, you can have one error check for application errors and system errors (e.g. disk full) after a transaction commits instead of an error check for DB.Begin() and DB.Commit().
3. If you forget to close a read transaction then it will keep old pages around to maintain consistency but this will cause your database to grow without bound. If you use the helper functions then you're protected from this even in the event of a panic().
The API is modeled around database/sql where applicable. Let me know if you find anything confusing and I can update the documentation to make it more clear.
Never really tried yet, but I think when using database/sql.Tx, the following snippet would be quite idiomatic, and I believe it should be perfectly safe:
tx, err := mydb.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec('...')
if err != nil {
return err
}
return tx.Commit()https://github.com/boltdb/bolt#managing-transactions-manuall...
The "defer tx.Rollback()" makes me cringe a little since it's confusing when you read the code top-down. It also throws out the error returned from Rollback(). Granted, the only possible error is if the DB is closed so it's not a big deal in most situations.
func action(...) (err error) {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("preparing transaction: %v", err)
}
defer func(tx *sql.Tx) {
if err != nil {
_ = tx.Rollback()
} else {
err = tx.Commit()
}
}(tx)
// do something
return
}If you take a look at the code, db.Begin is available and gives you access to the underlying transaction. You can use it as you want, however you'll need to be aware of catching any error, rollbacking, committing etc... all that is taken care of for you in db.Update (or db.View for read-only stuff)
This style is also consistent with the rest of the standard library, such as filepath.Walk or sort.Search.
Actually, going the "db.Begin" way would be more consistent with the rest of the standard library. Most of the Go APIs go to great length to be "external" (i.e. iterator-like), e.g. bufio.Scanner, and (of special note in this case!) database/sql.Tx! It appears to me the "internal"/callback APIs are introduced in those places where there are really compelling reasons, which justify breaking the consistency: e.g. for filepath.Walk, there's recurrence, so it's mostly giving up to the obviously K.I.S.S. way; in sort.Search, the benefit is that callback allows to evade need for generics/reflection.
https://cloud.google.com/appengine/docs/go/datastore/transac...
I remember that was a common pattern at Google when creating APIs interfacing with BigTable which also is a KV store. The premise is that you don't need to load everything into memory at once and instead do your work (for Update operations) or just pick interesting fields (for View operations) in smaller increments in your callback function.
> functions within functions, that can't really be refactored out or reused
This is functional programming but the choice of this paradigm does not impact code reuse. You can pass proper functions or methods and don't have to stick to lambdas.
Overall I think the concept is spot on.
As others points out there's nothing wrong the Bolt code, it's fine and pretty common, I just don't like it.
This is how I'd do it.
http://developer.amd.com/tools-and-sdks/opencl-zone/bolt-c-t...
But presuming Go is a server language, and you are running stateless web nodes, why do you need persistence?
Go can be used for far more than just web nodes.. it can be used for any number of network or stand-alone applications.