Skip to content

Commit

Permalink
add a BadgerDB backend (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdan authored Jul 9, 2020
1 parent 2a5ebd2 commit ae40198
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ linters:
- gosimple
- govet
- ineffassign
- interfacer
# - interfacer
- lll
- misspell
- maligned
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- [\#115](https://github.com/tendermint/tm-db/pull/115) Add a `BadgerDB` backend (@mvdan)

## 0.6.0

**2020-06-24**
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Go 1.13+

- **[RocksDB](https://github.com/tecbot/gorocksdb) [experimental]:** A [Go wrapper](https://github.com/tecbot/gorocksdb) around [RocksDB](https://rocksdb.org). Similarly to LevelDB (above) it uses LSM-trees for on-disk storage, but is optimized for fast storage media such as SSDs and memory. Supports atomic transactions, but not full ACID transactions.

- **[BadgerDB](https://github.com/dgraph-io/badger) [experimental]:** A key-value database written as a pure-Go alternative to others like LevelDB and RocksDB. Makes use of multiple goroutines for performance, and includes advanced features such as transactions, write batches, compression, and more.

## Meta-databases

- **PrefixDB [stable]:** A database which wraps another database and uses a static prefix for all keys. This allows multiple logical databases to be stored in a common underlying databases by using different namespaces. Used by the Cosmos SDK to give different modules their own namespaced database in a single application database.
Expand Down
16 changes: 16 additions & 0 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,22 @@ func testDBIterator(t *testing.T, backend BackendType) {
require.NoError(t, err)
verifyIterator(t, ritr,
[]int64(nil), "reverse iterator from 2 (ex) to 4")

// Ensure that the iterators don't panic with an empty database.
dir2, err := ioutil.TempDir("", "tm-db-test")
require.NoError(t, err)
db2, err := NewDB(name, backend, dir2)
require.NoError(t, err)
defer cleanupDBDir(dir2, name)

itr, err = db2.Iterator(nil, nil)
require.NoError(t, err)
verifyIterator(t, itr, nil, "forward iterator with empty db")

ritr, err = db2.ReverseIterator(nil, nil)
require.NoError(t, err)
verifyIterator(t, ritr, nil, "reverse iterator with empty db")

}

func verifyIterator(t *testing.T, itr Iterator, expected []int64, msg string) {
Expand Down
292 changes: 292 additions & 0 deletions badger_db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
package db

import (
"bytes"
"fmt"
"os"
"path/filepath"

"github.com/dgraph-io/badger/v2"
)

func init() { registerDBCreator(BadgerDBBackend, badgerDBCreator, true) }

func badgerDBCreator(dbName, dir string) (DB, error) {
return NewBadgerDB(dbName, dir)
}

// NewBadgerDB creates a Badger key-value store backed to the
// directory dir supplied. If dir does not exist, it will be created.
func NewBadgerDB(dbName, dir string) (*BadgerDB, error) {
// Since Badger doesn't support database names, we join both to obtain
// the final directory to use for the database.
path := filepath.Join(dir, dbName)

if err := os.MkdirAll(path, 0755); err != nil {
return nil, err
}
opts := badger.DefaultOptions(path)
opts.SyncWrites = false // note that we have Sync methods
opts.Logger = nil // badger is too chatty by default
return NewBadgerDBWithOptions(opts)
}

// NewBadgerDBWithOptions creates a BadgerDB key value store
// gives the flexibility of initializing a database with the
// respective options.
func NewBadgerDBWithOptions(opts badger.Options) (*BadgerDB, error) {
db, err := badger.Open(opts)
if err != nil {
return nil, err
}
return &BadgerDB{db: db}, nil
}

type BadgerDB struct {
db *badger.DB
}

var _ DB = (*BadgerDB)(nil)

func (b *BadgerDB) Get(key []byte) ([]byte, error) {
if len(key) == 0 {
return nil, errKeyEmpty
}
var val []byte
err := b.db.View(func(txn *badger.Txn) error {
item, err := txn.Get(key)
if err == badger.ErrKeyNotFound {
return nil
} else if err != nil {
return err
}
val, err = item.ValueCopy(nil)
if err == nil && val == nil {
val = []byte{}
}
return err
})
return val, err
}

func (b *BadgerDB) Has(key []byte) (bool, error) {
if len(key) == 0 {
return false, errKeyEmpty
}
var found bool
err := b.db.View(func(txn *badger.Txn) error {
_, err := txn.Get(key)
if err != nil && err != badger.ErrKeyNotFound {
return err
}
found = (err != badger.ErrKeyNotFound)
return nil
})
return found, err
}

func (b *BadgerDB) Set(key, value []byte) error {
if len(key) == 0 {
return errKeyEmpty
}
if value == nil {
return errValueNil
}
return b.db.Update(func(txn *badger.Txn) error {
return txn.Set(key, value)
})
}

func withSync(db *badger.DB, err error) error {
if err != nil {
return err
}
return db.Sync()
}

func (b *BadgerDB) SetSync(key, value []byte) error {
return withSync(b.db, b.Set(key, value))
}

func (b *BadgerDB) Delete(key []byte) error {
if len(key) == 0 {
return errKeyEmpty
}
return b.db.Update(func(txn *badger.Txn) error {
return txn.Delete(key)
})
}

func (b *BadgerDB) DeleteSync(key []byte) error {
return withSync(b.db, b.Delete(key))
}

func (b *BadgerDB) Close() error {
return b.db.Close()
}

func (b *BadgerDB) Print() error {
return nil
}

func (b *BadgerDB) iteratorOpts(start, end []byte, opts badger.IteratorOptions) (*badgerDBIterator, error) {
if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) {
return nil, errKeyEmpty
}
txn := b.db.NewTransaction(false)
iter := txn.NewIterator(opts)
iter.Rewind()
iter.Seek(start)
if opts.Reverse && iter.Valid() && bytes.Equal(iter.Item().Key(), start) {
// If we're going in reverse, our starting point was "end",
// which is exclusive.
iter.Next()
}
return &badgerDBIterator{
reverse: opts.Reverse,
start: start,
end: end,

txn: txn,
iter: iter,
}, nil
}

func (b *BadgerDB) Iterator(start, end []byte) (Iterator, error) {
opts := badger.DefaultIteratorOptions
return b.iteratorOpts(start, end, opts)
}

func (b *BadgerDB) ReverseIterator(start, end []byte) (Iterator, error) {
opts := badger.DefaultIteratorOptions
opts.Reverse = true
return b.iteratorOpts(end, start, opts)
}

func (b *BadgerDB) Stats() map[string]string {
return nil
}

func (b *BadgerDB) NewBatch() Batch {
wb := &badgerDBBatch{
db: b.db,
wb: b.db.NewWriteBatch(),
firstFlush: make(chan struct{}, 1),
}
wb.firstFlush <- struct{}{}
return wb
}

var _ Batch = (*badgerDBBatch)(nil)

type badgerDBBatch struct {
db *badger.DB
wb *badger.WriteBatch

// Calling db.Flush twice panics, so we must keep track of whether we've
// flushed already on our own. If Write can receive from the firstFlush
// channel, then it's the first and only Flush call we should do.
//
// Upstream bug report:
// https://github.com/dgraph-io/badger/issues/1394
firstFlush chan struct{}
}

func (b *badgerDBBatch) Set(key, value []byte) error {
if len(key) == 0 {
return errKeyEmpty
}
if value == nil {
return errValueNil
}
return b.wb.Set(key, value)
}

func (b *badgerDBBatch) Delete(key []byte) error {
if len(key) == 0 {
return errKeyEmpty
}
return b.wb.Delete(key)
}

func (b *badgerDBBatch) Write() error {
select {
case <-b.firstFlush:
return b.wb.Flush()
default:
return fmt.Errorf("batch already flushed")
}
}

func (b *badgerDBBatch) WriteSync() error {
return withSync(b.db, b.Write())
}

func (b *badgerDBBatch) Close() error {
select {
case <-b.firstFlush: // a Flush after Cancel panics too
default:
}
b.wb.Cancel()
return nil
}

type badgerDBIterator struct {
reverse bool
start, end []byte

txn *badger.Txn
iter *badger.Iterator

lastErr error
}

func (i *badgerDBIterator) Close() error {
i.iter.Close()
i.txn.Discard()
return nil
}

func (i *badgerDBIterator) Domain() (start, end []byte) { return i.start, i.end }
func (i *badgerDBIterator) Error() error { return i.lastErr }

func (i *badgerDBIterator) Next() {
if !i.Valid() {
panic("iterator is invalid")
}
i.iter.Next()
}

func (i *badgerDBIterator) Valid() bool {
if !i.iter.Valid() {
return false
}
if len(i.end) > 0 {
key := i.iter.Item().Key()
if c := bytes.Compare(key, i.end); (!i.reverse && c >= 0) || (i.reverse && c < 0) {
// We're at the end key, or past the end.
return false
}
}
return true
}

func (i *badgerDBIterator) Key() []byte {
if !i.Valid() {
panic("iterator is invalid")
}
// Note that we don't use KeyCopy, so this is only valid until the next
// call to Next.
return i.iter.Item().KeyCopy(nil)
}

func (i *badgerDBIterator) Value() []byte {
if !i.Valid() {
panic("iterator is invalid")
}
val, err := i.iter.Item().ValueCopy(nil)
if err != nil {
i.lastErr = err
}
return val
}
2 changes: 2 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
// - requires gcc
// - use rocksdb build tag (go build -tags rocksdb)
RocksDBBackend BackendType = "rocksdb"

BadgerDBBackend BackendType = "badgerdb"
)

type dbCreator func(name string, dir string) (DB, error)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/tendermint/tm-db
go 1.12

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/badger/v2 v2.0.3
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
Expand Down
Loading

0 comments on commit ae40198

Please sign in to comment.