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

Calling non-Close() methods on closed or written batches panic #71

Merged
merged 17 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

### Breaking Changes

- `Batch` interface methods now return errors
erikgrinaker marked this conversation as resolved.
Show resolved Hide resolved

- Closed batches can no longer be reused, all non-`Close()` calls return `ErrBatchClosed`

- Calling `Write()` or `WriteSync()` on a `Batch` will implicitly close it

- Removed the `SetDeleter` interface

- [memdb] [\#56](https://github.com/tendermint/tm-db/pull/56) Removed some exported methods that were mainly meant for internal use: `Mutex()`, `SetNoLock()`, `SetNoLockSync()`, `DeleteNoLock()`, and `DeleteNoLockSync()`

### Improvements
Expand All @@ -18,8 +26,6 @@

- [cleveldb] Fix handling of empty keys as iterator endpoints

- [goleveldb] [\#58](https://github.com/tendermint/tm-db/pull/58) Make `Batch.Close()` actually remove the batch contents

## 0.4.1

**2020-2-26**
Expand Down
86 changes: 42 additions & 44 deletions backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,69 +430,67 @@ func testDBBatch(t *testing.T, backend BackendType) {

// create a new batch, and some items - they should not be visible until we write
batch := db.NewBatch()
batch.Set([]byte("a"), []byte{1})
batch.Set([]byte("b"), []byte{2})
batch.Set([]byte("c"), []byte{3})
require.NoError(t, batch.Set([]byte("a"), []byte{1}))
require.NoError(t, batch.Set([]byte("b"), []byte{2}))
require.NoError(t, batch.Set([]byte("c"), []byte{3}))
assertKeyValues(t, db, map[string][]byte{})

err := batch.Write()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}, "c": {3}})

// the batch still keeps these values internally, so changing values and rewriting batch
// should set the values again
err = db.Set([]byte("a"), []byte{9})
require.NoError(t, err)
err = db.Delete([]byte("c"))
require.NoError(t, err)
// writing a batch should implicitly close it, so writing it again should error
err = batch.WriteSync()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}, "c": {3}})
require.Equal(t, ErrBatchClosed, err)

// but when we close, it should no longer set the values
batch.Close()
err = db.Delete([]byte("c"))
require.NoError(t, err)
err = batch.Write()
require.NoError(t, err)
// batches should write changes in order
batch = db.NewBatch()
require.NoError(t, batch.Delete([]byte("a")))
require.NoError(t, batch.Set([]byte("a"), []byte{1}))
require.NoError(t, batch.Set([]byte("b"), []byte{1}))
require.NoError(t, batch.Set([]byte("b"), []byte{2}))
require.NoError(t, batch.Set([]byte("c"), []byte{3}))
require.NoError(t, batch.Delete([]byte("c")))
require.NoError(t, batch.Write())
require.NoError(t, batch.Close())
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}})

// it should be possible to re-close the batch
batch.Close()

// it should also be possible to reuse a closed batch as if it were a new one
batch.Set([]byte("c"), []byte{3})
err = batch.Write()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}, "c": {3}})
batch.Close()

batch.Delete([]byte("c"))
err = batch.WriteSync()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}})
batch.Close()
// writing nil keys and values should be the same as empty keys and values
// FIXME CLevelDB panics here: https://github.com/jmhodges/levigo/issues/55
if backend != CLevelDBBackend {
batch = db.NewBatch()
err = batch.Set(nil, nil)
require.NoError(t, err)
err = batch.WriteSync()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"": {}, "a": {1}, "b": {2}})

batch = db.NewBatch()
err = batch.Delete(nil)
require.NoError(t, err)
err = batch.Write()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}})
}

// batches should also write changes in order
// it should be possible to write an empty batch
batch = db.NewBatch()
batch.Delete([]byte("a"))
batch.Set([]byte("a"), []byte{1})
batch.Set([]byte("b"), []byte{1})
batch.Set([]byte("b"), []byte{2})
batch.Set([]byte("c"), []byte{3})
batch.Delete([]byte("c"))
err = batch.Write()
require.NoError(t, err)
batch.Close()
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}})

// and writing an empty batch should not fail
// it should be possible to close an empty batch, and to re-close a closed batch
batch = db.NewBatch()
err = batch.Write()
err = batch.Close()
require.NoError(t, err)
err = batch.WriteSync()
err = batch.Close()
require.NoError(t, err)
assertKeyValues(t, db, map[string][]byte{"a": {1}, "b": {2}})

// all other operations on a closed batch should error
require.Equal(t, ErrBatchClosed, batch.Set([]byte("a"), []byte{9}))
require.Equal(t, ErrBatchClosed, batch.Delete([]byte("a")))
require.Equal(t, ErrBatchClosed, batch.Write())
require.Equal(t, ErrBatchClosed, batch.WriteSync())
}

func assertKeyValues(t *testing.T, db DB, expect map[string][]byte) {
Expand Down
5 changes: 1 addition & 4 deletions boltdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,7 @@ func (bdb *BoltDB) Stats() map[string]string {

// NewBatch implements DB.
func (bdb *BoltDB) NewBatch() Batch {
return &boltDBBatch{
ops: nil,
db: bdb,
}
return newBoltDBBatch(bdb)
}

// WARNING: Any concurrent writes or reads will block until the iterator is
Expand Down
33 changes: 29 additions & 4 deletions boltdb_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,37 @@ type boltDBBatch struct {

var _ Batch = (*boltDBBatch)(nil)

func newBoltDBBatch(db *BoltDB) *boltDBBatch {
return &boltDBBatch{
db: db,
ops: make([]operation, 0, 1),
erikgrinaker marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Set implements Batch.
func (b *boltDBBatch) Set(key, value []byte) {
func (b *boltDBBatch) Set(key, value []byte) error {
if b.ops == nil {
return ErrBatchClosed
erikgrinaker marked this conversation as resolved.
Show resolved Hide resolved
}
b.ops = append(b.ops, operation{opTypeSet, key, value})
return nil
}

// Delete implements Batch.
func (b *boltDBBatch) Delete(key []byte) {
func (b *boltDBBatch) Delete(key []byte) error {
if b.ops == nil {
return ErrBatchClosed
}
b.ops = append(b.ops, operation{opTypeDelete, key, nil})
return nil
}

// Write implements Batch.
func (b *boltDBBatch) Write() error {
return b.db.db.Batch(func(tx *bbolt.Tx) error {
if b.ops == nil {
return ErrBatchClosed
}
err := b.db.db.Batch(func(tx *bbolt.Tx) error {
bkt := tx.Bucket(bucket)
for _, op := range b.ops {
key := nonEmptyKey(nonNilBytes(op.key))
Expand All @@ -41,6 +59,10 @@ func (b *boltDBBatch) Write() error {
}
return nil
})
if err != nil {
return err
}
return b.Close()
}

// WriteSync implements Batch.
Expand All @@ -49,4 +71,7 @@ func (b *boltDBBatch) WriteSync() error {
}

// Close implements Batch.
func (b *boltDBBatch) Close() {}
func (b *boltDBBatch) Close() error {
b.ops = nil
return nil
}
3 changes: 1 addition & 2 deletions cleveldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ func (db *CLevelDB) Stats() map[string]string {

// NewBatch implements DB.
func (db *CLevelDB) NewBatch() Batch {
batch := levigo.NewWriteBatch()
return &cLevelDBBatch{db, batch}
return newCLevelDBBatch(db)
}

// Iterator implements DB.
Expand Down
47 changes: 37 additions & 10 deletions cleveldb_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,60 @@ type cLevelDBBatch struct {
batch *levigo.WriteBatch
}

func newCLevelDBBatch(db *CLevelDB) *cLevelDBBatch {
return &cLevelDBBatch{
db: db,
batch: levigo.NewWriteBatch(),
}
}

// Set implements Batch.
func (b *cLevelDBBatch) Set(key, value []byte) {
b.batch.Put(key, value)
func (b *cLevelDBBatch) Set(key, value []byte) error {
if b.batch == nil {
return ErrBatchClosed
}
b.batch.Put(nonNilBytes(key), nonNilBytes(value))
return nil
}

// Delete implements Batch.
func (b *cLevelDBBatch) Delete(key []byte) {
b.batch.Delete(key)
func (b *cLevelDBBatch) Delete(key []byte) error {
if b.batch == nil {
return ErrBatchClosed
}
b.batch.Delete(nonNilBytes(key))
return nil
}

// Write implements Batch.
func (b *cLevelDBBatch) Write() error {
if err := b.db.db.Write(b.db.wo, b.batch); err != nil {
if b.batch == nil {
return ErrBatchClosed
}
err := b.db.db.Write(b.db.wo, b.batch)
if err != nil {
return err
}
return nil
return b.Close()
Copy link
Contributor

Choose a reason for hiding this comment

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

do we even need a separate Close method? if we execute Close in Write/WriteSync

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, because the caller may e.g. return an error while they are building the batch, in which case they need to close it without calling Write.

Copy link
Contributor

Choose a reason for hiding this comment

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

I can easily envision cases where Close will be called twice... maybe it's okay, but I'd prefer to remove Close calls from Write/WriteSync. You're mixing responsibilities

Copy link
Contributor Author

@erikgrinaker erikgrinaker Mar 10, 2020

Choose a reason for hiding this comment

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

Calling Close() twice is fine, it is idempotent. It is a normal Go pattern to use defer c.Close() for error handling and then also do an explicit c.Close() as early as possible to free up resources before the function exits.

If we don't call Close() in Write(), then what should happen when a caller calls Write() twice?

Copy link
Contributor

Choose a reason for hiding this comment

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

It is a normal Go pattern to use defer c.Close() for error handling and then also do an explicit c.Close() as early as possible to free up resources before the function exits.

Didn't know about such.

If we don't call Close() in Write(), then what should happen when a caller calls Write() twice?

you can't prevent people from shooting themselves into the leg. I'd assume correct usage in most cases.

New -> Set/Delete -> Write(Sync) -> Close (via defer)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you can't prevent people from shooting themselves into the leg. I'd assume correct usage in most cases.

Of course, we just have to specify what should happen, as unspecified behavior is the root of all evil. Either Write() also calls Close(), in which case a second Write() call errors, otherwise the second Write()call should write the same batch again.

I think erroring is safer, and easier to implement across all backends. The typical and recommended usage would still be the same flow that you outline.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think erroring is safer, and easier to implement across all backends.

Agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I think this should be good to go then.

}

// WriteSync implements Batch.
func (b *cLevelDBBatch) WriteSync() error {
if err := b.db.db.Write(b.db.woSync, b.batch); err != nil {
if b.batch == nil {
return ErrBatchClosed
}
err := b.db.db.Write(b.db.woSync, b.batch)
if err != nil {
return err
}
return nil
return b.Close()
}

// Close implements Batch.
func (b *cLevelDBBatch) Close() {
b.batch.Close()
func (b *cLevelDBBatch) Close() error {
if b.batch != nil {
b.batch.Close()
b.batch = nil
}
return nil
}
3 changes: 1 addition & 2 deletions goleveldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ func (db *GoLevelDB) Stats() map[string]string {

// NewBatch implements DB.
func (db *GoLevelDB) NewBatch() Batch {
batch := new(leveldb.Batch)
return &goLevelDBBatch{db, batch}
return newGoLevelDBBatch(db)
}

// Iterator implements DB.
Expand Down
37 changes: 31 additions & 6 deletions goleveldb_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,60 @@ type goLevelDBBatch struct {

var _ Batch = (*goLevelDBBatch)(nil)

func newGoLevelDBBatch(db *GoLevelDB) *goLevelDBBatch {
return &goLevelDBBatch{
db: db,
batch: new(leveldb.Batch),
}
}

// Set implements Batch.
func (b *goLevelDBBatch) Set(key, value []byte) {
func (b *goLevelDBBatch) Set(key, value []byte) error {
if b.batch == nil {
return ErrBatchClosed
}
b.batch.Put(key, value)
return nil
}

// Delete implements Batch.
func (b *goLevelDBBatch) Delete(key []byte) {
func (b *goLevelDBBatch) Delete(key []byte) error {
if b.batch == nil {
return ErrBatchClosed
}
b.batch.Delete(key)
return nil
}

// Write implements Batch.
func (b *goLevelDBBatch) Write() error {
if b.batch == nil {
return ErrBatchClosed
}
err := b.db.db.Write(b.batch, &opt.WriteOptions{Sync: false})
if err != nil {
return err
}
return nil
return b.Close()
}

// WriteSync implements Batch.
func (b *goLevelDBBatch) WriteSync() error {
if b.batch == nil {
return ErrBatchClosed
}
err := b.db.db.Write(b.batch, &opt.WriteOptions{Sync: true})
if err != nil {
return err
}
return nil
return b.Close()
}

// Close implements Batch.
func (b *goLevelDBBatch) Close() {
b.batch.Reset()
func (b *goLevelDBBatch) Close() error {
if b.batch != nil {
b.batch.Reset()
b.batch = nil
}
return nil
}
2 changes: 1 addition & 1 deletion memdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (db *MemDB) Stats() map[string]string {

// NewBatch implements DB.
func (db *MemDB) NewBatch() Batch {
return &memDBBatch{db, nil}
return newMemDBBatch(db)
}

// Iterator implements DB.
Expand Down
Loading