From 22de16c8e211a122225ee5868ff64b01bf837c3f Mon Sep 17 00:00:00 2001 From: Sarah Christoff Date: Fri, 26 Mar 2021 16:37:52 -0500 Subject: [PATCH 1/4] First Pass Replace --- bolt_store.go | 8 ++++---- bolt_store_test.go | 16 ++++++++-------- go.mod | 4 ++-- go.sum | 4 ++++ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/bolt_store.go b/bolt_store.go index 66b75bc..a5822da 100644 --- a/bolt_store.go +++ b/bolt_store.go @@ -3,7 +3,7 @@ package raftboltdb import ( "errors" - "github.com/boltdb/bolt" + "go.etcd.io/bbolt" "github.com/hashicorp/raft" ) @@ -27,7 +27,7 @@ var ( // a LogStore and StableStore. type BoltStore struct { // conn is the underlying handle to the db. - conn *bolt.DB + conn *bbolt.DB // The path to the Bolt database file path string @@ -40,7 +40,7 @@ type Options struct { // BoltOptions contains any specific BoltDB options you might // want to specify [e.g. open timeout] - BoltOptions *bolt.Options + BoltOptions *bbolt.Options // NoSync causes the database to skip fsync calls after each // write to the log. This is unsafe, so it should be used @@ -63,7 +63,7 @@ func NewBoltStore(path string) (*BoltStore, error) { // New uses the supplied options to open the BoltDB and prepare it for use as a raft backend. func New(options Options) (*BoltStore, error) { // Try to connect - handle, err := bolt.Open(options.Path, dbFileMode, options.BoltOptions) + handle, err := bbolt.Open(options.Path, dbFileMode, options.BoltOptions) if err != nil { return nil, err } diff --git a/bolt_store_test.go b/bolt_store_test.go index 12b09b2..737efc2 100644 --- a/bolt_store_test.go +++ b/bolt_store_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/boltdb/bolt" + "go.etcd.io/bbolt" "github.com/hashicorp/raft" ) @@ -54,7 +54,7 @@ func TestBoltOptionsTimeout(t *testing.T) { defer os.Remove(fh.Name()) options := Options{ Path: fh.Name(), - BoltOptions: &bolt.Options{ + BoltOptions: &bbolt.Options{ Timeout: time.Second / 10, }, } @@ -102,7 +102,7 @@ func TestBoltOptionsReadOnly(t *testing.T) { store.Close() options := Options{ Path: fh.Name(), - BoltOptions: &bolt.Options{ + BoltOptions: &bbolt.Options{ Timeout: time.Second / 10, ReadOnly: true, }, @@ -123,8 +123,8 @@ func TestBoltOptionsReadOnly(t *testing.T) { } // Attempt to store the log, should fail on a read-only store err = roStore.StoreLog(log) - if err != bolt.ErrDatabaseReadOnly { - t.Errorf("expecting error %v, but got %v", bolt.ErrDatabaseReadOnly, err) + if err != bbolt.ErrDatabaseReadOnly { + t.Errorf("expecting error %v, but got %v", bbolt.ErrDatabaseReadOnly, err) } } @@ -156,7 +156,7 @@ func TestNewBoltStore(t *testing.T) { } // Ensure our tables were created - db, err := bolt.Open(fh.Name(), dbFileMode, nil) + db, err := bbolt.Open(fh.Name(), dbFileMode, nil) if err != nil { t.Fatalf("err: %s", err) } @@ -164,10 +164,10 @@ func TestNewBoltStore(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - if _, err := tx.CreateBucket([]byte(dbLogs)); err != bolt.ErrBucketExists { + if _, err := tx.CreateBucket([]byte(dbLogs)); err != bbolt.ErrBucketExists { t.Fatalf("bad: %v", err) } - if _, err := tx.CreateBucket([]byte(dbConf)); err != bolt.ErrBucketExists { + if _, err := tx.CreateBucket([]byte(dbConf)); err != bbolt.ErrBucketExists { t.Fatalf("bad: %v", err) } } diff --git a/go.mod b/go.mod index 5b92dc7..3b29579 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/hashicorp/raft-boltdb go 1.12 require ( - github.com/boltdb/bolt v1.3.1 + // github.com/boltdb/bolt v1.3.1 github.com/hashicorp/go-msgpack v0.5.5 github.com/hashicorp/raft v1.1.0 - golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed // indirect + go.etcd.io/bbolt v1.3.5 ) diff --git a/go.sum b/go.sum index 6433ce8..b746757 100644 --- a/go.sum +++ b/go.sum @@ -39,7 +39,11 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 60b5b00d04ec181b24019f3ace1cc4c14292a986 Mon Sep 17 00:00:00 2001 From: Sarah Christoff Date: Fri, 26 Mar 2021 17:45:02 -0500 Subject: [PATCH 2/4] begone with you --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 3b29579..29cd470 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/hashicorp/raft-boltdb go 1.12 require ( - // github.com/boltdb/bolt v1.3.1 github.com/hashicorp/go-msgpack v0.5.5 github.com/hashicorp/raft v1.1.0 go.etcd.io/bbolt v1.3.5 From cc05784fd1bbfdf113169c6ad2d4e0206dea790f Mon Sep 17 00:00:00 2001 From: Sarah Christoff Date: Mon, 29 Mar 2021 20:45:18 -0500 Subject: [PATCH 3/4] v2 addition --- bolt_store.go | 8 +- bolt_store_test.go | 16 +- go.mod | 2 +- go.sum | 6 +- v2/README.md | 5 + v2/bench_test.go | 88 +++++++++ v2/bolt_store.go | 268 +++++++++++++++++++++++++++ v2/bolt_store_test.go | 416 ++++++++++++++++++++++++++++++++++++++++++ v2/go.mod | 9 + v2/go.sum | 39 ++++ v2/util.go | 37 ++++ 11 files changed, 876 insertions(+), 18 deletions(-) create mode 100644 v2/README.md create mode 100644 v2/bench_test.go create mode 100644 v2/bolt_store.go create mode 100644 v2/bolt_store_test.go create mode 100644 v2/go.mod create mode 100644 v2/go.sum create mode 100644 v2/util.go diff --git a/bolt_store.go b/bolt_store.go index a5822da..66b75bc 100644 --- a/bolt_store.go +++ b/bolt_store.go @@ -3,7 +3,7 @@ package raftboltdb import ( "errors" - "go.etcd.io/bbolt" + "github.com/boltdb/bolt" "github.com/hashicorp/raft" ) @@ -27,7 +27,7 @@ var ( // a LogStore and StableStore. type BoltStore struct { // conn is the underlying handle to the db. - conn *bbolt.DB + conn *bolt.DB // The path to the Bolt database file path string @@ -40,7 +40,7 @@ type Options struct { // BoltOptions contains any specific BoltDB options you might // want to specify [e.g. open timeout] - BoltOptions *bbolt.Options + BoltOptions *bolt.Options // NoSync causes the database to skip fsync calls after each // write to the log. This is unsafe, so it should be used @@ -63,7 +63,7 @@ func NewBoltStore(path string) (*BoltStore, error) { // New uses the supplied options to open the BoltDB and prepare it for use as a raft backend. func New(options Options) (*BoltStore, error) { // Try to connect - handle, err := bbolt.Open(options.Path, dbFileMode, options.BoltOptions) + handle, err := bolt.Open(options.Path, dbFileMode, options.BoltOptions) if err != nil { return nil, err } diff --git a/bolt_store_test.go b/bolt_store_test.go index 737efc2..12b09b2 100644 --- a/bolt_store_test.go +++ b/bolt_store_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "go.etcd.io/bbolt" + "github.com/boltdb/bolt" "github.com/hashicorp/raft" ) @@ -54,7 +54,7 @@ func TestBoltOptionsTimeout(t *testing.T) { defer os.Remove(fh.Name()) options := Options{ Path: fh.Name(), - BoltOptions: &bbolt.Options{ + BoltOptions: &bolt.Options{ Timeout: time.Second / 10, }, } @@ -102,7 +102,7 @@ func TestBoltOptionsReadOnly(t *testing.T) { store.Close() options := Options{ Path: fh.Name(), - BoltOptions: &bbolt.Options{ + BoltOptions: &bolt.Options{ Timeout: time.Second / 10, ReadOnly: true, }, @@ -123,8 +123,8 @@ func TestBoltOptionsReadOnly(t *testing.T) { } // Attempt to store the log, should fail on a read-only store err = roStore.StoreLog(log) - if err != bbolt.ErrDatabaseReadOnly { - t.Errorf("expecting error %v, but got %v", bbolt.ErrDatabaseReadOnly, err) + if err != bolt.ErrDatabaseReadOnly { + t.Errorf("expecting error %v, but got %v", bolt.ErrDatabaseReadOnly, err) } } @@ -156,7 +156,7 @@ func TestNewBoltStore(t *testing.T) { } // Ensure our tables were created - db, err := bbolt.Open(fh.Name(), dbFileMode, nil) + db, err := bolt.Open(fh.Name(), dbFileMode, nil) if err != nil { t.Fatalf("err: %s", err) } @@ -164,10 +164,10 @@ func TestNewBoltStore(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - if _, err := tx.CreateBucket([]byte(dbLogs)); err != bbolt.ErrBucketExists { + if _, err := tx.CreateBucket([]byte(dbLogs)); err != bolt.ErrBucketExists { t.Fatalf("bad: %v", err) } - if _, err := tx.CreateBucket([]byte(dbConf)); err != bbolt.ErrBucketExists { + if _, err := tx.CreateBucket([]byte(dbConf)); err != bolt.ErrBucketExists { t.Fatalf("bad: %v", err) } } diff --git a/go.mod b/go.mod index 29cd470..3ae4d9d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/hashicorp/raft-boltdb go 1.12 require ( + github.com/boltdb/bolt v1.3.1 github.com/hashicorp/go-msgpack v0.5.5 github.com/hashicorp/raft v1.1.0 - go.etcd.io/bbolt v1.3.5 ) diff --git a/go.sum b/go.sum index b746757..b34e552 100644 --- a/go.sum +++ b/go.sum @@ -39,11 +39,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= \ No newline at end of file diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..cdeda18 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,5 @@ +raft-boltdb/v2 +=========== + +This implementation uses the maintained version of BoltDB, [BBolt](https://github.com/etcd-io/bbolt). This is the primary version of `raft-boltdb` and should be used whenever possible. + diff --git a/v2/bench_test.go b/v2/bench_test.go new file mode 100644 index 0000000..b860706 --- /dev/null +++ b/v2/bench_test.go @@ -0,0 +1,88 @@ +package raftboltdb + +import ( + "os" + "testing" + + "github.com/hashicorp/raft/bench" +) + +func BenchmarkBoltStore_FirstIndex(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.FirstIndex(b, store) +} + +func BenchmarkBoltStore_LastIndex(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.LastIndex(b, store) +} + +func BenchmarkBoltStore_GetLog(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.GetLog(b, store) +} + +func BenchmarkBoltStore_StoreLog(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.StoreLog(b, store) +} + +func BenchmarkBoltStore_StoreLogs(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.StoreLogs(b, store) +} + +func BenchmarkBoltStore_DeleteRange(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.DeleteRange(b, store) +} + +func BenchmarkBoltStore_Set(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.Set(b, store) +} + +func BenchmarkBoltStore_Get(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.Get(b, store) +} + +func BenchmarkBoltStore_SetUint64(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.SetUint64(b, store) +} + +func BenchmarkBoltStore_GetUint64(b *testing.B) { + store := testBoltStore(b) + defer store.Close() + defer os.Remove(store.path) + + raftbench.GetUint64(b, store) +} diff --git a/v2/bolt_store.go b/v2/bolt_store.go new file mode 100644 index 0000000..a5822da --- /dev/null +++ b/v2/bolt_store.go @@ -0,0 +1,268 @@ +package raftboltdb + +import ( + "errors" + + "go.etcd.io/bbolt" + "github.com/hashicorp/raft" +) + +const ( + // Permissions to use on the db file. This is only used if the + // database file does not exist and needs to be created. + dbFileMode = 0600 +) + +var ( + // Bucket names we perform transactions in + dbLogs = []byte("logs") + dbConf = []byte("conf") + + // An error indicating a given key does not exist + ErrKeyNotFound = errors.New("not found") +) + +// BoltStore provides access to BoltDB for Raft to store and retrieve +// log entries. It also provides key/value storage, and can be used as +// a LogStore and StableStore. +type BoltStore struct { + // conn is the underlying handle to the db. + conn *bbolt.DB + + // The path to the Bolt database file + path string +} + +// Options contains all the configuration used to open the BoltDB +type Options struct { + // Path is the file path to the BoltDB to use + Path string + + // BoltOptions contains any specific BoltDB options you might + // want to specify [e.g. open timeout] + BoltOptions *bbolt.Options + + // NoSync causes the database to skip fsync calls after each + // write to the log. This is unsafe, so it should be used + // with caution. + NoSync bool +} + +// readOnly returns true if the contained bolt options say to open +// the DB in readOnly mode [this can be useful to tools that want +// to examine the log] +func (o *Options) readOnly() bool { + return o != nil && o.BoltOptions != nil && o.BoltOptions.ReadOnly +} + +// NewBoltStore takes a file path and returns a connected Raft backend. +func NewBoltStore(path string) (*BoltStore, error) { + return New(Options{Path: path}) +} + +// New uses the supplied options to open the BoltDB and prepare it for use as a raft backend. +func New(options Options) (*BoltStore, error) { + // Try to connect + handle, err := bbolt.Open(options.Path, dbFileMode, options.BoltOptions) + if err != nil { + return nil, err + } + handle.NoSync = options.NoSync + + // Create the new store + store := &BoltStore{ + conn: handle, + path: options.Path, + } + + // If the store was opened read-only, don't try and create buckets + if !options.readOnly() { + // Set up our buckets + if err := store.initialize(); err != nil { + store.Close() + return nil, err + } + } + return store, nil +} + +// initialize is used to set up all of the buckets. +func (b *BoltStore) initialize() error { + tx, err := b.conn.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + // Create all the buckets + if _, err := tx.CreateBucketIfNotExists(dbLogs); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(dbConf); err != nil { + return err + } + + return tx.Commit() +} + +// Close is used to gracefully close the DB connection. +func (b *BoltStore) Close() error { + return b.conn.Close() +} + +// FirstIndex returns the first known index from the Raft log. +func (b *BoltStore) FirstIndex() (uint64, error) { + tx, err := b.conn.Begin(false) + if err != nil { + return 0, err + } + defer tx.Rollback() + + curs := tx.Bucket(dbLogs).Cursor() + if first, _ := curs.First(); first == nil { + return 0, nil + } else { + return bytesToUint64(first), nil + } +} + +// LastIndex returns the last known index from the Raft log. +func (b *BoltStore) LastIndex() (uint64, error) { + tx, err := b.conn.Begin(false) + if err != nil { + return 0, err + } + defer tx.Rollback() + + curs := tx.Bucket(dbLogs).Cursor() + if last, _ := curs.Last(); last == nil { + return 0, nil + } else { + return bytesToUint64(last), nil + } +} + +// GetLog is used to retrieve a log from BoltDB at a given index. +func (b *BoltStore) GetLog(idx uint64, log *raft.Log) error { + tx, err := b.conn.Begin(false) + if err != nil { + return err + } + defer tx.Rollback() + + bucket := tx.Bucket(dbLogs) + val := bucket.Get(uint64ToBytes(idx)) + + if val == nil { + return raft.ErrLogNotFound + } + return decodeMsgPack(val, log) +} + +// StoreLog is used to store a single raft log +func (b *BoltStore) StoreLog(log *raft.Log) error { + return b.StoreLogs([]*raft.Log{log}) +} + +// StoreLogs is used to store a set of raft logs +func (b *BoltStore) StoreLogs(logs []*raft.Log) error { + tx, err := b.conn.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + for _, log := range logs { + key := uint64ToBytes(log.Index) + val, err := encodeMsgPack(log) + if err != nil { + return err + } + bucket := tx.Bucket(dbLogs) + if err := bucket.Put(key, val.Bytes()); err != nil { + return err + } + } + + return tx.Commit() +} + +// DeleteRange is used to delete logs within a given range inclusively. +func (b *BoltStore) DeleteRange(min, max uint64) error { + minKey := uint64ToBytes(min) + + tx, err := b.conn.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + curs := tx.Bucket(dbLogs).Cursor() + for k, _ := curs.Seek(minKey); k != nil; k, _ = curs.Next() { + // Handle out-of-range log index + if bytesToUint64(k) > max { + break + } + + // Delete in-range log index + if err := curs.Delete(); err != nil { + return err + } + } + + return tx.Commit() +} + +// Set is used to set a key/value set outside of the raft log +func (b *BoltStore) Set(k, v []byte) error { + tx, err := b.conn.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + bucket := tx.Bucket(dbConf) + if err := bucket.Put(k, v); err != nil { + return err + } + + return tx.Commit() +} + +// Get is used to retrieve a value from the k/v store by key +func (b *BoltStore) Get(k []byte) ([]byte, error) { + tx, err := b.conn.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + bucket := tx.Bucket(dbConf) + val := bucket.Get(k) + + if val == nil { + return nil, ErrKeyNotFound + } + return append([]byte(nil), val...), nil +} + +// SetUint64 is like Set, but handles uint64 values +func (b *BoltStore) SetUint64(key []byte, val uint64) error { + return b.Set(key, uint64ToBytes(val)) +} + +// GetUint64 is like Get, but handles uint64 values +func (b *BoltStore) GetUint64(key []byte) (uint64, error) { + val, err := b.Get(key) + if err != nil { + return 0, err + } + return bytesToUint64(val), nil +} + +// Sync performs an fsync on the database file handle. This is not necessary +// under normal operation unless NoSync is enabled, in which this forces the +// database file to sync against the disk. +func (b *BoltStore) Sync() error { + return b.conn.Sync() +} diff --git a/v2/bolt_store_test.go b/v2/bolt_store_test.go new file mode 100644 index 0000000..737efc2 --- /dev/null +++ b/v2/bolt_store_test.go @@ -0,0 +1,416 @@ +package raftboltdb + +import ( + "bytes" + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + "go.etcd.io/bbolt" + "github.com/hashicorp/raft" +) + +func testBoltStore(t testing.TB) *BoltStore { + fh, err := ioutil.TempFile("", "bolt") + if err != nil { + t.Fatalf("err: %s", err) + } + os.Remove(fh.Name()) + + // Successfully creates and returns a store + store, err := NewBoltStore(fh.Name()) + if err != nil { + t.Fatalf("err: %s", err) + } + + return store +} + +func testRaftLog(idx uint64, data string) *raft.Log { + return &raft.Log{ + Data: []byte(data), + Index: idx, + } +} + +func TestBoltStore_Implements(t *testing.T) { + var store interface{} = &BoltStore{} + if _, ok := store.(raft.StableStore); !ok { + t.Fatalf("BoltStore does not implement raft.StableStore") + } + if _, ok := store.(raft.LogStore); !ok { + t.Fatalf("BoltStore does not implement raft.LogStore") + } +} + +func TestBoltOptionsTimeout(t *testing.T) { + fh, err := ioutil.TempFile("", "bolt") + if err != nil { + t.Fatalf("err: %s", err) + } + os.Remove(fh.Name()) + defer os.Remove(fh.Name()) + options := Options{ + Path: fh.Name(), + BoltOptions: &bbolt.Options{ + Timeout: time.Second / 10, + }, + } + store, err := New(options) + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + // trying to open it again should timeout + doneCh := make(chan error, 1) + go func() { + _, err := New(options) + doneCh <- err + }() + select { + case err := <-doneCh: + if err == nil || err.Error() != "timeout" { + t.Errorf("Expected timeout error but got %v", err) + } + case <-time.After(5 * time.Second): + t.Errorf("Gave up waiting for timeout response") + } +} + +func TestBoltOptionsReadOnly(t *testing.T) { + fh, err := ioutil.TempFile("", "bolt") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh.Name()) + store, err := NewBoltStore(fh.Name()) + if err != nil { + t.Fatalf("err: %s", err) + } + // Create the log + log := &raft.Log{ + Data: []byte("log1"), + Index: 1, + } + // Attempt to store the log + if err := store.StoreLog(log); err != nil { + t.Fatalf("err: %s", err) + } + + store.Close() + options := Options{ + Path: fh.Name(), + BoltOptions: &bbolt.Options{ + Timeout: time.Second / 10, + ReadOnly: true, + }, + } + roStore, err := New(options) + if err != nil { + t.Fatalf("err: %s", err) + } + defer roStore.Close() + result := new(raft.Log) + if err := roStore.GetLog(1, result); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the log comes back the same + if !reflect.DeepEqual(log, result) { + t.Errorf("bad: %v", result) + } + // Attempt to store the log, should fail on a read-only store + err = roStore.StoreLog(log) + if err != bbolt.ErrDatabaseReadOnly { + t.Errorf("expecting error %v, but got %v", bbolt.ErrDatabaseReadOnly, err) + } +} + +func TestNewBoltStore(t *testing.T) { + fh, err := ioutil.TempFile("", "bolt") + if err != nil { + t.Fatalf("err: %s", err) + } + os.Remove(fh.Name()) + defer os.Remove(fh.Name()) + + // Successfully creates and returns a store + store, err := NewBoltStore(fh.Name()) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the file was created + if store.path != fh.Name() { + t.Fatalf("unexpected file path %q", store.path) + } + if _, err := os.Stat(fh.Name()); err != nil { + t.Fatalf("err: %s", err) + } + + // Close the store so we can open again + if err := store.Close(); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure our tables were created + db, err := bbolt.Open(fh.Name(), dbFileMode, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + tx, err := db.Begin(true) + if err != nil { + t.Fatalf("err: %s", err) + } + if _, err := tx.CreateBucket([]byte(dbLogs)); err != bbolt.ErrBucketExists { + t.Fatalf("bad: %v", err) + } + if _, err := tx.CreateBucket([]byte(dbConf)); err != bbolt.ErrBucketExists { + t.Fatalf("bad: %v", err) + } +} + +func TestBoltStore_FirstIndex(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Should get 0 index on empty log + idx, err := store.FirstIndex() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 0 { + t.Fatalf("bad: %v", idx) + } + + // Set a mock raft log + logs := []*raft.Log{ + testRaftLog(1, "log1"), + testRaftLog(2, "log2"), + testRaftLog(3, "log3"), + } + if err := store.StoreLogs(logs); err != nil { + t.Fatalf("bad: %s", err) + } + + // Fetch the first Raft index + idx, err = store.FirstIndex() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 1 { + t.Fatalf("bad: %d", idx) + } +} + +func TestBoltStore_LastIndex(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Should get 0 index on empty log + idx, err := store.LastIndex() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 0 { + t.Fatalf("bad: %v", idx) + } + + // Set a mock raft log + logs := []*raft.Log{ + testRaftLog(1, "log1"), + testRaftLog(2, "log2"), + testRaftLog(3, "log3"), + } + if err := store.StoreLogs(logs); err != nil { + t.Fatalf("bad: %s", err) + } + + // Fetch the last Raft index + idx, err = store.LastIndex() + if err != nil { + t.Fatalf("err: %s", err) + } + if idx != 3 { + t.Fatalf("bad: %d", idx) + } +} + +func TestBoltStore_GetLog(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + log := new(raft.Log) + + // Should return an error on non-existent log + if err := store.GetLog(1, log); err != raft.ErrLogNotFound { + t.Fatalf("expected raft log not found error, got: %v", err) + } + + // Set a mock raft log + logs := []*raft.Log{ + testRaftLog(1, "log1"), + testRaftLog(2, "log2"), + testRaftLog(3, "log3"), + } + if err := store.StoreLogs(logs); err != nil { + t.Fatalf("bad: %s", err) + } + + // Should return the proper log + if err := store.GetLog(2, log); err != nil { + t.Fatalf("err: %s", err) + } + if !reflect.DeepEqual(log, logs[1]) { + t.Fatalf("bad: %#v", log) + } +} + +func TestBoltStore_SetLog(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Create the log + log := &raft.Log{ + Data: []byte("log1"), + Index: 1, + } + + // Attempt to store the log + if err := store.StoreLog(log); err != nil { + t.Fatalf("err: %s", err) + } + + // Retrieve the log again + result := new(raft.Log) + if err := store.GetLog(1, result); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the log comes back the same + if !reflect.DeepEqual(log, result) { + t.Fatalf("bad: %v", result) + } +} + +func TestBoltStore_SetLogs(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Create a set of logs + logs := []*raft.Log{ + testRaftLog(1, "log1"), + testRaftLog(2, "log2"), + } + + // Attempt to store the logs + if err := store.StoreLogs(logs); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure we stored them all + result1, result2 := new(raft.Log), new(raft.Log) + if err := store.GetLog(1, result1); err != nil { + t.Fatalf("err: %s", err) + } + if !reflect.DeepEqual(logs[0], result1) { + t.Fatalf("bad: %#v", result1) + } + if err := store.GetLog(2, result2); err != nil { + t.Fatalf("err: %s", err) + } + if !reflect.DeepEqual(logs[1], result2) { + t.Fatalf("bad: %#v", result2) + } +} + +func TestBoltStore_DeleteRange(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Create a set of logs + log1 := testRaftLog(1, "log1") + log2 := testRaftLog(2, "log2") + log3 := testRaftLog(3, "log3") + logs := []*raft.Log{log1, log2, log3} + + // Attempt to store the logs + if err := store.StoreLogs(logs); err != nil { + t.Fatalf("err: %s", err) + } + + // Attempt to delete a range of logs + if err := store.DeleteRange(1, 2); err != nil { + t.Fatalf("err: %s", err) + } + + // Ensure the logs were deleted + if err := store.GetLog(1, new(raft.Log)); err != raft.ErrLogNotFound { + t.Fatalf("should have deleted log1") + } + if err := store.GetLog(2, new(raft.Log)); err != raft.ErrLogNotFound { + t.Fatalf("should have deleted log2") + } +} + +func TestBoltStore_Set_Get(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Returns error on non-existent key + if _, err := store.Get([]byte("bad")); err != ErrKeyNotFound { + t.Fatalf("expected not found error, got: %q", err) + } + + k, v := []byte("hello"), []byte("world") + + // Try to set a k/v pair + if err := store.Set(k, v); err != nil { + t.Fatalf("err: %s", err) + } + + // Try to read it back + val, err := store.Get(k) + if err != nil { + t.Fatalf("err: %s", err) + } + if !bytes.Equal(val, v) { + t.Fatalf("bad: %v", val) + } +} + +func TestBoltStore_SetUint64_GetUint64(t *testing.T) { + store := testBoltStore(t) + defer store.Close() + defer os.Remove(store.path) + + // Returns error on non-existent key + if _, err := store.GetUint64([]byte("bad")); err != ErrKeyNotFound { + t.Fatalf("expected not found error, got: %q", err) + } + + k, v := []byte("abc"), uint64(123) + + // Attempt to set the k/v pair + if err := store.SetUint64(k, v); err != nil { + t.Fatalf("err: %s", err) + } + + // Read back the value + val, err := store.GetUint64(k) + if err != nil { + t.Fatalf("err: %s", err) + } + if val != v { + t.Fatalf("bad: %v", val) + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..2b589b6 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,9 @@ +module github.com/hashicorp/raft-boltdb/v2 + +go 1.12 + +require ( + github.com/hashicorp/go-msgpack v0.5.5 + github.com/hashicorp/raft v1.1.0 + go.etcd.io/bbolt v1.3.5 +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..4ca5a0b --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,39 @@ +github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= +github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.1 h1:9PZfAcVEvez4yhLH2TBU64/h/z4xlFI80cWXRrxuKuM= +github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= +github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/raft v1.1.0 h1:qPMePEczgbkiQsqCsRfuHRqvDUO+zmAInDaD5ptXlq0= +github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/v2/util.go b/v2/util.go new file mode 100644 index 0000000..68dd786 --- /dev/null +++ b/v2/util.go @@ -0,0 +1,37 @@ +package raftboltdb + +import ( + "bytes" + "encoding/binary" + + "github.com/hashicorp/go-msgpack/codec" +) + +// Decode reverses the encode operation on a byte slice input +func decodeMsgPack(buf []byte, out interface{}) error { + r := bytes.NewBuffer(buf) + hd := codec.MsgpackHandle{} + dec := codec.NewDecoder(r, &hd) + return dec.Decode(out) +} + +// Encode writes an encoded object to a new bytes buffer +func encodeMsgPack(in interface{}) (*bytes.Buffer, error) { + buf := bytes.NewBuffer(nil) + hd := codec.MsgpackHandle{} + enc := codec.NewEncoder(buf, &hd) + err := enc.Encode(in) + return buf, err +} + +// Converts bytes to an integer +func bytesToUint64(b []byte) uint64 { + return binary.BigEndian.Uint64(b) +} + +// Converts a uint to a byte slice +func uint64ToBytes(u uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, u) + return buf +} From fba1f5c6dd8b886671a57e984bfb714bc2182d57 Mon Sep 17 00:00:00 2001 From: Sarah Christoff Date: Wed, 7 Apr 2021 15:25:05 -0500 Subject: [PATCH 4/4] update docs --- v2/README.md | 3 ++- v2/bolt_store.go | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/v2/README.md b/v2/README.md index cdeda18..a87b819 100644 --- a/v2/README.md +++ b/v2/README.md @@ -1,5 +1,6 @@ raft-boltdb/v2 =========== -This implementation uses the maintained version of BoltDB, [BBolt](https://github.com/etcd-io/bbolt). This is the primary version of `raft-boltdb` and should be used whenever possible. +This implementation uses the maintained version of BoltDB, [BBolt](https://github.com/etcd-io/bbolt). This is the primary version of `raft-boltdb` and should be used whenever possible. +There is no breaking API change to the library. However, there is the potential for disk format incompatibilities so it was decided to be conservative and making it a separate import path. This separate import path will allow both versions (original and v2) to be imported to perform a safe in-place upgrade of old files read with the old version and written back out with the new one. \ No newline at end of file diff --git a/v2/bolt_store.go b/v2/bolt_store.go index a5822da..0e365ee 100644 --- a/v2/bolt_store.go +++ b/v2/bolt_store.go @@ -3,8 +3,8 @@ package raftboltdb import ( "errors" - "go.etcd.io/bbolt" "github.com/hashicorp/raft" + "go.etcd.io/bbolt" ) const ( @@ -22,7 +22,7 @@ var ( ErrKeyNotFound = errors.New("not found") ) -// BoltStore provides access to BoltDB for Raft to store and retrieve +// BoltStore provides access to Bbolt for Raft to store and retrieve // log entries. It also provides key/value storage, and can be used as // a LogStore and StableStore. type BoltStore struct { @@ -33,12 +33,12 @@ type BoltStore struct { path string } -// Options contains all the configuration used to open the BoltDB +// Options contains all the configuration used to open the Bbolt type Options struct { - // Path is the file path to the BoltDB to use + // Path is the file path to the Bbolt to use Path string - // BoltOptions contains any specific BoltDB options you might + // BoltOptions contains any specific Bbolt options you might // want to specify [e.g. open timeout] BoltOptions *bbolt.Options @@ -60,7 +60,7 @@ func NewBoltStore(path string) (*BoltStore, error) { return New(Options{Path: path}) } -// New uses the supplied options to open the BoltDB and prepare it for use as a raft backend. +// New uses the supplied options to open the Bbolt and prepare it for use as a raft backend. func New(options Options) (*BoltStore, error) { // Try to connect handle, err := bbolt.Open(options.Path, dbFileMode, options.BoltOptions) @@ -142,7 +142,7 @@ func (b *BoltStore) LastIndex() (uint64, error) { } } -// GetLog is used to retrieve a log from BoltDB at a given index. +// GetLog is used to retrieve a log from Bbolt at a given index. func (b *BoltStore) GetLog(idx uint64, log *raft.Log) error { tx, err := b.conn.Begin(false) if err != nil {