initial commit

This commit is contained in:
ston1th 2020-11-14 17:44:52 +01:00
commit 9210ab3133
8 changed files with 791 additions and 0 deletions

24
LICENSE Normal file
View file

@ -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.

9
README.md Normal file
View file

@ -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.

9
go.mod Normal file
View file

@ -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
)

50
go.sum Normal file
View file

@ -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=

45
msgpack/msgpack.go Normal file
View file

@ -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)
}

223
store.go Normal file
View file

@ -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()
}

416
store_test.go Normal file
View file

@ -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)
}
}

15
util.go Normal file
View file

@ -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
}