commit 9210ab3133d16be708d0ce5175c591e075c5ff94 Author: ston1th Date: Sat Nov 14 17:44:52 2020 +0100 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dd57fd8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (C) 2020 Marius Schellenberger +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of the authors and/or contributors may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e2b662 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# raft-bbolt + +This work is based on https://github.com/hashicorp/raft-boltdb + +This repository provides the `raftbbolt` package. The package exports the `Store` which is an implementation of both a `LogStore` and `StableStore`. + +It is meant to be used as a backend for the `raft` [package here](https://github.com/hashicorp/raft). + +This implementation uses [BBolt](https://github.com/etcd-io/bbolt). BBolt is a simple key/value store implemented in pure Go, and inspired by LMDB. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d22bf4e --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.giftfish.de/ston1th/raftbbolt + +go 1.15 + +require ( + github.com/hashicorp/go-msgpack v1.1.5 + github.com/hashicorp/raft v1.2.0 + go.etcd.io/bbolt v1.3.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..47ed7a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +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/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +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/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= +github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= +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.2.0 h1:mHzHIrF0S91d3A7RPBvuqkgB4d/7oFJZyvf1Q4m7GA0= +github.com/hashicorp/raft v1.2.0/go.mod h1:vPAJM8Asw6u8LxC3eJCUZmRP/E4QmUGE1R7g7k8sG/8= +github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea/go.mod h1:pNv7Wc3ycL6F5oOWn+tPGo2gWD4a5X+yp/ntwdKLjRk= +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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190523142557-0e01d883c5c5/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= diff --git a/msgpack/msgpack.go b/msgpack/msgpack.go new file mode 100644 index 0000000..cc4e953 --- /dev/null +++ b/msgpack/msgpack.go @@ -0,0 +1,45 @@ +package msgpack + +import ( + "bytes" + "sync" + + "github.com/hashicorp/go-msgpack/codec" +) + +var bufPool = sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, +} + +var rdrPool = sync.Pool{ + New: func() interface{} { return new(bytes.Reader) }, +} + +type encoder struct{} + +var MsgPack = encoder{} + +func (encoder) Unmarshal(b []byte, v interface{}) error { + rdr := rdrPool.Get().(*bytes.Reader) + rdr.Reset(b) + defer rdrPool.Put(rdr) + return codec.NewDecoder(rdr, &codec.MsgpackHandle{}).Decode(v) +} + +func (encoder) Marshal(v interface{}) ([]byte, error) { + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + if err := codec.NewEncoder(buf, &codec.MsgpackHandle{}).Encode(v); err != nil { + bufPool.Put(buf) + return nil, err + } + return buf.Bytes(), nil +} + +func Unmarshal(b []byte, v interface{}) error { + return MsgPack.Unmarshal(b, v) +} + +func Marshal(v interface{}) ([]byte, error) { + return MsgPack.Marshal(v) +} diff --git a/store.go b/store.go new file mode 100644 index 0000000..874c6a0 --- /dev/null +++ b/store.go @@ -0,0 +1,223 @@ +package raftbbolt + +import ( + "errors" + + "git.giftfish.de/ston1th/raftbbolt/msgpack" + "github.com/hashicorp/raft" + "go.etcd.io/bbolt" +) + +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") +) + +// Store 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 Store struct { + // conn is the underlying handle to the db. + db *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 +} + +// NewStore takes a file path and returns a connected Raft backend. +func NewStore(path string) (*Store, 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) (*Store, error) { + db, err := bbolt.Open(options.Path, dbFileMode, options.BoltOptions) + if err != nil { + return nil, err + } + db.NoSync = options.NoSync + + // Create the new store + store := &Store{ + db: db, + 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 (s *Store) initialize() error { + return s.db.Update(func(tx *bbolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists(dbLogs); err != nil { + return err + } + _, err := tx.CreateBucketIfNotExists(dbConf) + return err + }) +} + +// Close is used to gracefully close the DB connection. +func (s *Store) Close() error { + return s.db.Close() +} + +// FirstIndex returns the first known index from the Raft log. +func (s *Store) FirstIndex() (n uint64, err error) { + err = s.db.View(func(tx *bbolt.Tx) error { + c := tx.Bucket(dbLogs).Cursor() + first, _ := c.First() + if first == nil { + return nil + } + n = bytesToUint64(first) + return nil + }) + return +} + +// LastIndex returns the last known index from the Raft log. +func (s *Store) LastIndex() (n uint64, err error) { + err = s.db.View(func(tx *bbolt.Tx) error { + c := tx.Bucket(dbLogs).Cursor() + last, _ := c.Last() + if last == nil { + return nil + } + n = bytesToUint64(last) + return nil + }) + return +} + +// GetLog is used to retrieve a log from BoltDB at a given index. +func (s *Store) GetLog(idx uint64, log *raft.Log) error { + return s.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(dbLogs).Get(uint64ToBytes(idx)) + if b == nil { + return raft.ErrLogNotFound + } + return msgpack.Unmarshal(b, log) + }) +} + +// StoreLog is used to store a single raft log +func (s *Store) StoreLog(log *raft.Log) error { + return s.StoreLogs([]*raft.Log{log}) +} + +// StoreLogs is used to store a set of raft logs +func (s *Store) StoreLogs(logs []*raft.Log) error { + return s.db.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(dbLogs) + for _, log := range logs { + b, err := msgpack.Marshal(log) + if err != nil { + return err + } + if err := bucket.Put(uint64ToBytes(log.Index), b); err != nil { + return err + } + } + return nil + }) +} + +// DeleteRange is used to delete logs within a given range inclusively. +func (s *Store) DeleteRange(min, max uint64) error { + return s.db.Update(func(tx *bbolt.Tx) error { + c := tx.Bucket(dbLogs).Cursor() + minKey := uint64ToBytes(min) + for k, _ := c.Seek(minKey); k != nil; k, _ = c.Next() { + // Handle out-of-range log index + if bytesToUint64(k) > max { + break + } + // Delete in-range log index + if err := c.Delete(); err != nil { + return err + } + } + return nil + }) +} + +// Set is used to set a key/value set outside of the raft log +func (s *Store) Set(k, v []byte) error { + return s.db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(dbConf).Put(k, v) + }) +} + +// Get is used to retrieve a value from the k/v store by key +func (s *Store) Get(k []byte) (b []byte, err error) { + err = s.db.View(func(tx *bbolt.Tx) error { + b = tx.Bucket(dbConf).Get(k) + if b == nil { + return ErrKeyNotFound + } + return nil + }) + return +} + +// SetUint64 is like Set, but handles uint64 values +func (s *Store) SetUint64(k []byte, v uint64) error { + return s.Set(k, uint64ToBytes(v)) +} + +// GetUint64 is like Get, but handles uint64 values +func (s *Store) GetUint64(k []byte) (uint64, error) { + b, err := s.Get(k) + if err != nil { + return 0, err + } + return bytesToUint64(b), 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 (s *Store) Sync() error { + return s.db.Sync() +} diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000..0dca5d7 --- /dev/null +++ b/store_test.go @@ -0,0 +1,416 @@ +package raftbbolt + +import ( + "bytes" + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + "github.com/hashicorp/raft" + "go.etcd.io/bbolt" +) + +func testStore(t testing.TB) *Store { + 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 := NewStore(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 TestStore_Implements(t *testing.T) { + var store interface{} = &Store{} + if _, ok := store.(raft.StableStore); !ok { + t.Fatalf("Store does not implement raft.StableStore") + } + if _, ok := store.(raft.LogStore); !ok { + t.Fatalf("Store does not implement raft.LogStore") + } +} + +func TestOptionsTimeout(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 TestOptionsReadOnly(t *testing.T) { + fh, err := ioutil.TempFile("", "bolt") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh.Name()) + store, err := NewStore(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 TestNewStore(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 := NewStore(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 TestStore_FirstIndex(t *testing.T) { + store := testStore(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 TestStore_LastIndex(t *testing.T) { + store := testStore(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 TestStore_GetLog(t *testing.T) { + store := testStore(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 TestStore_SetLog(t *testing.T) { + store := testStore(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 TestStore_SetLogs(t *testing.T) { + store := testStore(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 TestStore_DeleteRange(t *testing.T) { + store := testStore(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 TestStore_Set_Get(t *testing.T) { + store := testStore(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 TestStore_SetUint64_GetUint64(t *testing.T) { + store := testStore(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/util.go b/util.go new file mode 100644 index 0000000..adc40ec --- /dev/null +++ b/util.go @@ -0,0 +1,15 @@ +package raftbbolt + +import ( + "encoding/binary" +) + +func bytesToUint64(b []byte) uint64 { + return binary.BigEndian.Uint64(b) +} + +func uint64ToBytes(u uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, u) + return buf +}