initial commit
This commit is contained in:
commit
9210ab3133
8 changed files with 791 additions and 0 deletions
24
LICENSE
Normal file
24
LICENSE
Normal 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
9
README.md
Normal 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
9
go.mod
Normal 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
50
go.sum
Normal 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
45
msgpack/msgpack.go
Normal 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
223
store.go
Normal 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
416
store_test.go
Normal 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
15
util.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue