├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── badger_store.go ├── badger_store_test.go ├── bench_test.go ├── go.mod └── go.sum /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.11 6 | steps: 7 | - checkout 8 | - run: go mod download 9 | - run: go test -race -cover -coverprofile=coverage.txt . 10 | - run: go test -bench . 11 | - run: bash <(curl -s https://codecov.io/bash) 12 | - store_test_results: 13 | path: coverage.txt 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2018-02-22 9 | 10 | ### Added 11 | 12 | - add basic implementation of StableStore/LogStore interfaces 13 | - add tests 14 | - add benchmark 15 | - add README 16 | - add CI config 17 | - add badges 18 | - add coverage 19 | 20 | ### Changed 21 | 22 | - initial release, changes will be included in future versions 23 | 24 | ### Removed 25 | 26 | - n/a 27 | 28 | [unreleased]: https://github.com/markthethomas/raft-badger/compare/v1.0.0...HEAD 29 | [1.0.0]: https://github.com/markthethomas/raft-badger/releases/tag/1.0.0 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mark Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raft-badger 2 | 3 | [![CircleCI](https://circleci.com/gh/markthethomas/raft-badger/tree/master.svg?style=svg)](https://circleci.com/gh/markthethomas/raft-badger/tree/master) [![Go Report Card](https://goreportcard.com/badge/github.com/markthethomas/raft-badger)](https://goreportcard.com/report/github.com/markthethomas/raft-badger) [![Maintainability](https://api.codeclimate.com/v1/badges/2aef013ae290d9233ac5/maintainability)](https://codeclimate.com/github/markthethomas/raft-badger/maintainability) [![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/markthethomas/raft-badger.svg?maxAge=2592000)](https://codecov.io/github/markthethomas/raft-badger?branch=master) [![HitCount](http://hits.dwyl.io/markthethomas/github.com/markthethomas/raft-badger.svg)](http://hits.dwyl.io/markthethomas/github.com/markthethomas/raft-badger) [![GoDoc](https://godoc.org/github.com/markthethomas/raft-badger?status.png)](https://godoc.org/github.com/markthethomas/raft-badger) 4 | 5 | ![Raft + Badger backend plugin](https://cdn.ifelse.io/images/raft-badger.png) 6 | 7 | This repository provides a storage backend for the excellent [raft package](https://github.com/hashicorp/raft) from Hashicorp. Raft is a [distributed consensus](https://en.wikipedia.org/wiki/Consensus_(computer_science)) protocol. Distributed consensus has *many* practical applications, ranging from fault-tolerant databases to clock synchronization to things like Google's PageRank. 8 | 9 | This package exports the `BadgerStore`, which is an implementation of both a `LogStore` and `StableStore` (interfaces used by the raft package for reading/writing logs as part of its consensus protocol). 10 | 11 | - [raft-badger](#raft-badger) 12 | - [installation](#installation) 13 | - [usage](#usage) 14 | - [developing](#developing) 15 | - [motivation](#motivation) 16 | - [misc.](#misc) 17 | - [todo](#todo) 18 | 19 | ## installation 20 | 21 | ```bash 22 | go get -u github.com/markthethomas/raft-badger 23 | ``` 24 | 25 | ## usage 26 | 27 | Create a new BadgerStore and pass it to Raft when setting up. 28 | 29 | ```go 30 | //... 31 | var logStore raft.LogStore 32 | var stableStore raft.StableStore 33 | myPath := filepath.Join(s.RaftDir) // replace this with what you're actually using 34 | badgerDB, err := raftbadgerdb.NewBadgerStore(myPath) 35 | if err != nil { 36 | return fmt.Errorf("error creating new badger store: %s", err) 37 | } 38 | logStore = badgerDB 39 | stableStore = badgerDB 40 | 41 | r, err := raft.NewRaft(config, (*fsm)(s), logStore, stableStore, snapshots, transport) 42 | //... 43 | ``` 44 | 45 | ## developing 46 | 47 | To run tests, run: 48 | 49 | ```bash 50 | go test -cover -coverprofile=coverage.out . 51 | ``` 52 | 53 | To view coverage, run: 54 | 55 | ```bash 56 | go tool cover -html=coverage.out 57 | ``` 58 | 59 | To run the benchmark, run: 60 | 61 | ```bash 62 | go test -race -bench . 63 | ``` 64 | 65 | ## motivation 66 | 67 | This package is meant to be used with the [raft package](https://github.com/hashicorp/raft) from Hashicorb. This package borrows heavily from the excellent [raft-boltdb](https://github.com/hashicorp/raft-boltdb) package, also from Hashicorp. I wanted to learn about Badger and similar tools and needed to use Raft + a durable backend. 68 | 69 | ## misc. 70 | 71 | - raft-badger uses prefix keys to "bucket" logs and config, avoiding the need for multiple badger database files for each type of k/v raft sets 72 | - encodes/decodes the raft [Log](https://godoc.org/github.com/hashicorp/raft#Log) types using Go's [gob](https://golang.org/pkg/encoding/gob/) for efficient encoding/decoding of keys See more at https://blog.golang.org/gobs-of-data. 73 | - images used are from the [raft website](https://raft.github.io) and [the badger repository](https://github.com/dgraph-io/badger), respectively 74 | - thanks to the authors of the excellent [raft-boltdb](https://github.com/hashicorp/raft-boltdb) package for providing patterns to follow in satisfying the requisite raft interfaces 🙌 75 | - curious to learn more about the raft protocol? check out [the raft website](https://raft.github.io). There's also a beginner's guide at [Free Code Camp](https://medium.freecodecamp.org/in-search-of-an-understandable-consensus-algorithm-a-summary-4bc294c97e0d) 76 | 77 | ## todo 78 | 79 | - support custom badger options 80 | - explore other encodings besides `gob` 81 | - add more examples of use with raft 82 | -------------------------------------------------------------------------------- /badger_store.go: -------------------------------------------------------------------------------- 1 | package raftbadgerdb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/gob" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "math" 11 | "strconv" 12 | 13 | "github.com/dgraph-io/badger" 14 | "github.com/hashicorp/raft" 15 | ) 16 | 17 | var ( 18 | // Bucket names we perform transactions in 19 | dbLogsPrefix = []byte("logs") 20 | dbConfPrefix = []byte("conf") 21 | 22 | // ErrKeyNotFound is an error indicating a given key does not exist 23 | ErrKeyNotFound = errors.New("not found") 24 | ) 25 | 26 | // BadgerStore provides access to Badger for Raft to store and retrieve 27 | // log entries. It also provides key/value storage, and can be used as 28 | // a LogStore and StableStore. See https://godoc.org/github.com/hashicorp/raft#StableStore 29 | // and https://godoc.org/github.com/hashicorp/raft#LogStore 30 | type BadgerStore struct { 31 | db *badger.DB 32 | path string 33 | } 34 | 35 | // Options contains all the configuration used to open BadgerDB 36 | type Options struct { 37 | // BadgerOptions contains any Badger-specific options 38 | BadgerOptions *badger.Options 39 | // Path is the directory 40 | Path string 41 | } 42 | 43 | // NewBadgerStore takes a file path and returns a connected Raft backend. 44 | func NewBadgerStore(path string) (*BadgerStore, error) { 45 | opts := Options{Path: path, BadgerOptions: &badger.DefaultOptions} 46 | return New(opts) 47 | } 48 | 49 | // New uses the supplied options to open a badger db and prepare it for use as a raft backend. 50 | func New(options Options) (*BadgerStore, error) { 51 | options.BadgerOptions.Dir = options.Path + "/badger" 52 | options.BadgerOptions.ValueDir = options.Path + "/badger" 53 | db, err := badger.Open(*options.BadgerOptions) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | store := &BadgerStore{ 59 | db: db, 60 | path: options.Path, 61 | } 62 | return store, nil 63 | } 64 | 65 | // Close is used to gracefully close the DB connection. 66 | func (b *BadgerStore) Close() error { 67 | return b.db.Close() 68 | } 69 | 70 | func bytesToUint64(b []byte) uint64 { 71 | return binary.BigEndian.Uint64(b) 72 | } 73 | 74 | // Converts a uint to a byte slice 75 | func uint64ToBytes(u uint64) []byte { 76 | buf := make([]byte, 8) 77 | binary.BigEndian.PutUint64(buf, u) 78 | return buf 79 | } 80 | 81 | // FirstIndex returns the first known index from the Raft log. 82 | func (b *BadgerStore) FirstIndex() (uint64, error) { 83 | first := uint64(0) 84 | err := b.db.View(func(txn *badger.Txn) error { 85 | it := txn.NewIterator(badger.DefaultIteratorOptions) 86 | defer it.Close() 87 | it.Seek(dbLogsPrefix) 88 | if it.ValidForPrefix(dbLogsPrefix) { 89 | item := it.Item() 90 | k := string(item.Key()[len(dbLogsPrefix):]) 91 | idx, err := strconv.ParseUint(k, 10, 64) 92 | if err != nil { 93 | return err 94 | } 95 | first = idx 96 | } 97 | return nil 98 | }) 99 | if err != nil { 100 | return 0, err 101 | } 102 | return first, nil 103 | } 104 | 105 | // LastIndex returns the last known index from the Raft log. 106 | func (b *BadgerStore) LastIndex() (uint64, error) { 107 | last := uint64(0) 108 | if err := b.db.View(func(txn *badger.Txn) error { 109 | opts := badger.DefaultIteratorOptions 110 | opts.Reverse = true 111 | it := txn.NewIterator(opts) 112 | defer it.Close() 113 | // ensure reverse seeking will include the 114 | // see https://github.com/dgraph-io/badger/issues/436 and 115 | // https://github.com/dgraph-io/badger/issues/347 116 | seekKey := append(dbLogsPrefix, 0xFF) 117 | it.Seek(seekKey) 118 | if it.ValidForPrefix(dbLogsPrefix) { 119 | item := it.Item() 120 | k := string(item.Key()[len(dbLogsPrefix):]) 121 | idx, err := strconv.ParseUint(k, 10, 64) 122 | if err != nil { 123 | return err 124 | } 125 | last = idx 126 | } 127 | return nil 128 | }); err != nil { 129 | return 0, err 130 | } 131 | return last, nil 132 | } 133 | 134 | // GetLog is used to retrieve a log from Badger at a given index. 135 | func (b *BadgerStore) GetLog(idx uint64, log *raft.Log) error { 136 | return b.db.View(func(txn *badger.Txn) error { 137 | item, err := txn.Get([]byte(fmt.Sprintf("%s%d", dbLogsPrefix, idx))) 138 | if item == nil { 139 | return raft.ErrLogNotFound 140 | } 141 | v, err := item.Value() 142 | if err != nil { 143 | return err 144 | } 145 | buf := bytes.NewBuffer(v) 146 | dec := gob.NewDecoder(buf) 147 | return dec.Decode(&log) 148 | }) 149 | } 150 | 151 | // StoreLog is used to store a single raft log 152 | func (b *BadgerStore) StoreLog(log *raft.Log) error { 153 | return b.StoreLogs([]*raft.Log{log}) 154 | } 155 | 156 | // StoreLogs is used to store a set of raft logs 157 | func (b *BadgerStore) StoreLogs(logs []*raft.Log) error { 158 | maxBatchSize := b.db.MaxBatchSize() 159 | min := uint64(0) 160 | max := uint64(len(logs)) 161 | ranges := b.generateRanges(min, max, maxBatchSize) 162 | for _, r := range ranges { 163 | txn := b.db.NewTransaction(true) 164 | defer txn.Discard() 165 | for index := r.from; index < r.to; index++ { 166 | log := logs[index] 167 | key := []byte(fmt.Sprintf("%s%d", dbLogsPrefix, log.Index)) 168 | var out bytes.Buffer 169 | enc := gob.NewEncoder(&out) 170 | enc.Encode(log) 171 | if err := txn.Set(key, out.Bytes()); err != nil { 172 | return err 173 | } 174 | } 175 | if err := txn.Commit(nil); err != nil { 176 | return err 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | type iteratorRange struct{ from, to uint64 } 183 | 184 | func (b *BadgerStore) generateRanges(min, max uint64, batchSize int64) []iteratorRange { 185 | nSegments := int(math.Round(float64((max - min) / uint64(batchSize)))) 186 | segments := []iteratorRange{} 187 | if (max - min) <= uint64(batchSize) { 188 | segments = append(segments, iteratorRange{from: min, to: max}) 189 | return segments 190 | } 191 | for len(segments) < nSegments { 192 | nextMin := min + uint64(batchSize) 193 | segments = append(segments, iteratorRange{from: min, to: nextMin}) 194 | min = nextMin + 1 195 | } 196 | segments = append(segments, iteratorRange{from: min, to: max}) 197 | return segments 198 | } 199 | 200 | // DeleteRange is used to delete logs within a given range inclusively. 201 | func (b *BadgerStore) DeleteRange(min, max uint64) error { 202 | maxBatchSize := b.db.MaxBatchSize() 203 | ranges := b.generateRanges(min, max, maxBatchSize) 204 | for _, r := range ranges { 205 | txn := b.db.NewTransaction(true) 206 | it := txn.NewIterator(badger.DefaultIteratorOptions) 207 | defer txn.Discard() 208 | 209 | it.Rewind() 210 | // Get the key to start at 211 | minKey := []byte(fmt.Sprintf("%s%d", dbLogsPrefix, r.from)) 212 | for it.Seek(minKey); it.ValidForPrefix(dbLogsPrefix); it.Next() { 213 | item := it.Item() 214 | // get the index as a string to convert to uint64 215 | k := string(item.Key()[len(dbLogsPrefix):]) 216 | idx, err := strconv.ParseUint(k, 10, 64) 217 | if err != nil { 218 | it.Close() 219 | return err 220 | } 221 | // Handle out-of-range index 222 | if idx > r.to { 223 | break 224 | } 225 | // Delete in-range index 226 | delKey := []byte(fmt.Sprintf("%s%d", dbLogsPrefix, idx)) 227 | if err := txn.Delete(delKey); err != nil { 228 | it.Close() 229 | return err 230 | } 231 | } 232 | it.Close() 233 | if err := txn.Commit(nil); err != nil { 234 | return err 235 | } 236 | } 237 | return nil 238 | } 239 | 240 | // Set is used to set a key/value set outside of the raft log 241 | func (b *BadgerStore) Set(k, v []byte) error { 242 | return b.db.Update(func(txn *badger.Txn) error { 243 | key := []byte(fmt.Sprintf("%s%d", dbConfPrefix, k)) 244 | return txn.Set(key, v) 245 | }) 246 | } 247 | 248 | // Get is used to retrieve a value from the k/v store by key 249 | func (b *BadgerStore) Get(k []byte) ([]byte, error) { 250 | txn := b.db.NewTransaction(false) 251 | defer txn.Discard() 252 | key := []byte(fmt.Sprintf("%s%d", dbConfPrefix, k)) 253 | item, err := txn.Get(key) 254 | if item == nil { 255 | return nil, ErrKeyNotFound 256 | } 257 | if err != nil { 258 | return nil, err 259 | } 260 | v, err := item.ValueCopy(nil) 261 | if err != nil { 262 | return nil, err 263 | } 264 | if err := txn.Commit(nil); err != nil { 265 | return nil, err 266 | } 267 | return append([]byte(nil), v...), nil 268 | } 269 | 270 | // SetUint64 is like Set, but handles uint64 values 271 | func (b *BadgerStore) SetUint64(key []byte, val uint64) error { 272 | return b.Set(key, uint64ToBytes(val)) 273 | } 274 | 275 | // GetUint64 is like Get, but handles uint64 values 276 | func (b *BadgerStore) GetUint64(key []byte) (uint64, error) { 277 | val, err := b.Get(key) 278 | if err != nil { 279 | return 0, err 280 | } 281 | return bytesToUint64(val), nil 282 | } 283 | -------------------------------------------------------------------------------- /badger_store_test.go: -------------------------------------------------------------------------------- 1 | package raftbadgerdb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/hashicorp/raft" 12 | ) 13 | 14 | func testBadgerStore(t testing.TB) *BadgerStore { 15 | fh, err := ioutil.TempDir("", "badger") 16 | if err != nil { 17 | t.Fatalf("err: %s", err) 18 | } 19 | store, err := NewBadgerStore(fh) 20 | if err != nil { 21 | t.Fatalf("err: %s", err) 22 | } 23 | return store 24 | } 25 | 26 | func testRaftLog(idx uint64, data string) *raft.Log { 27 | return &raft.Log{ 28 | Data: []byte(data), 29 | Index: idx, 30 | } 31 | } 32 | 33 | func TestBadgerStore_Implements(t *testing.T) { 34 | var store interface{} = &BadgerStore{} 35 | if _, ok := store.(raft.StableStore); !ok { 36 | t.Fatalf("BadgerStore does not implement raft.StableStore") 37 | } 38 | if _, ok := store.(raft.LogStore); !ok { 39 | t.Fatalf("BadgerStore does not implement raft.LogStore") 40 | } 41 | } 42 | 43 | func TestNewBadgerStore(t *testing.T) { 44 | fh, err := ioutil.TempDir("", "badger") 45 | if err != nil { 46 | t.Fatalf("err: %s", err) 47 | } 48 | // os.Remove(fh) 49 | defer os.Remove(fh) 50 | 51 | // Successfully creates and returns a store 52 | store, err := NewBadgerStore(fh) 53 | if err != nil { 54 | t.Fatalf("err: %s", err) 55 | } 56 | 57 | // Ensure the file was created 58 | if store.path != fh { 59 | t.Fatalf("unexpected file path %q", store.path) 60 | } 61 | if _, err := os.Stat(fh); err != nil { 62 | t.Fatalf("err: %s", err) 63 | } 64 | 65 | // Close the store so we can open again 66 | if err := store.Close(); err != nil { 67 | t.Fatalf("err: %s", err) 68 | } 69 | } 70 | 71 | func TestBadgerStore_FirstIndex(t *testing.T) { 72 | store := testBadgerStore(t) 73 | defer store.Close() 74 | defer os.Remove(store.path) 75 | 76 | // Should get 0 index on empty log 77 | idx, err := store.FirstIndex() 78 | if err != nil { 79 | t.Fatalf("err: %s", err) 80 | } 81 | if idx != 0 { 82 | t.Fatalf("bad: %v", idx) 83 | } 84 | 85 | // Set a mock raft log 86 | logs := []*raft.Log{ 87 | testRaftLog(1, "log1"), 88 | testRaftLog(2, "log2"), 89 | testRaftLog(3, "log3"), 90 | } 91 | if err := store.StoreLogs(logs); err != nil { 92 | fmt.Println("YOOO") 93 | t.Fatalf("bad: %s", err) 94 | } 95 | 96 | // Fetch the first Raft index 97 | idx, err = store.FirstIndex() 98 | if err != nil { 99 | t.Fatalf("err: %s", err) 100 | } 101 | if idx != 1 { 102 | t.Fatalf("bad: %d", idx) 103 | } 104 | } 105 | 106 | func TestBadgerStore_LastIndex(t *testing.T) { 107 | store := testBadgerStore(t) 108 | defer store.Close() 109 | defer os.Remove(store.path) 110 | 111 | // Should get 0 index on empty log 112 | idx, err := store.LastIndex() 113 | if err != nil { 114 | t.Fatalf("err: %s", err) 115 | } 116 | if idx != 0 { 117 | t.Fatalf("bad: %v", idx) 118 | } 119 | 120 | // Set a mock raft log 121 | logs := []*raft.Log{ 122 | testRaftLog(1, "log1"), 123 | testRaftLog(2, "log2"), 124 | testRaftLog(3, "log3"), 125 | } 126 | if err := store.StoreLogs(logs); err != nil { 127 | t.Fatalf("bad: %s", err) 128 | } 129 | 130 | // Fetch the last Raft index 131 | idx, err = store.LastIndex() 132 | if err != nil { 133 | t.Fatalf("err: %s", err) 134 | } 135 | if idx != 3 { 136 | t.Fatalf("bad final index, got: %d", idx) 137 | } 138 | } 139 | 140 | func TestBadgerStore_GetLog(t *testing.T) { 141 | store := testBadgerStore(t) 142 | defer os.Remove(store.path) 143 | defer store.Close() 144 | 145 | log := new(raft.Log) 146 | 147 | // Should return an error on non-existent log 148 | if err := store.GetLog(1, log); err != raft.ErrLogNotFound { 149 | t.Fatalf("expected raft log not found error, got: %v", err) 150 | } 151 | 152 | // Set a mock raft log 153 | logs := []*raft.Log{ 154 | testRaftLog(1, "log1"), 155 | testRaftLog(2, "log2"), 156 | testRaftLog(3, "log3"), 157 | testRaftLog(4, "log3"), 158 | } 159 | if err := store.StoreLogs(logs); err != nil { 160 | t.Fatalf("bad: %s", err) 161 | } 162 | 163 | // Should return the proper log 164 | if err := store.GetLog(2, log); err != nil { 165 | t.Fatalf("err: %s", err) 166 | } 167 | if !reflect.DeepEqual(log, logs[1]) { 168 | t.Fatalf("bad: %#v", log) 169 | } 170 | } 171 | 172 | func TestBadgerStore_SetLog(t *testing.T) { 173 | store := testBadgerStore(t) 174 | defer store.Close() 175 | defer os.Remove(store.path) 176 | 177 | // Create the log 178 | log := &raft.Log{ 179 | Data: []byte("log1"), 180 | Index: 1, 181 | } 182 | 183 | // Attempt to store the log 184 | if err := store.StoreLog(log); err != nil { 185 | t.Fatalf("err: %s", err) 186 | } 187 | 188 | // Retrieve the log again 189 | result := new(raft.Log) 190 | if err := store.GetLog(1, result); err != nil { 191 | t.Fatalf("err: %s", err) 192 | } 193 | 194 | // Ensure the log comes back the same 195 | if !reflect.DeepEqual(log, result) { 196 | t.Fatalf("bad: %v", result) 197 | } 198 | } 199 | 200 | func TestBadgerStore_SetLogs(t *testing.T) { 201 | store := testBadgerStore(t) 202 | defer store.Close() 203 | defer os.Remove(store.path) 204 | 205 | // Create a set of logs 206 | logs := []*raft.Log{ 207 | testRaftLog(1, "log1"), 208 | testRaftLog(2, "log2"), 209 | } 210 | 211 | // Attempt to store the logs 212 | if err := store.StoreLogs(logs); err != nil { 213 | t.Fatalf("err: %s", err) 214 | } 215 | 216 | // Ensure we stored them all 217 | result1, result2 := new(raft.Log), new(raft.Log) 218 | if err := store.GetLog(1, result1); err != nil { 219 | t.Fatalf("err: %s", err) 220 | } 221 | if !reflect.DeepEqual(logs[0], result1) { 222 | t.Fatalf("bad: %#v", result1) 223 | } 224 | if err := store.GetLog(2, result2); err != nil { 225 | t.Fatalf("err: %s", err) 226 | } 227 | if !reflect.DeepEqual(logs[1], result2) { 228 | t.Fatalf("bad: %#v", result2) 229 | } 230 | } 231 | 232 | func TestBadgerStore_DeleteRange(t *testing.T) { 233 | store := testBadgerStore(t) 234 | defer store.Close() 235 | defer os.Remove(store.path) 236 | 237 | // Create a set of logs 238 | log1 := testRaftLog(1, "log1") 239 | log2 := testRaftLog(2, "log2") 240 | log3 := testRaftLog(3, "log3") 241 | logs := []*raft.Log{log1, log2, log3} 242 | 243 | // Attempt to store the logs 244 | if err := store.StoreLogs(logs); err != nil { 245 | t.Fatalf("err: %s", err) 246 | } 247 | 248 | // Attempt to delete a range of logs 249 | if err := store.DeleteRange(1, 2); err != nil { 250 | t.Fatalf("err: %s", err) 251 | } 252 | 253 | // Ensure the logs were deleted 254 | if err := store.GetLog(1, new(raft.Log)); err != raft.ErrLogNotFound { 255 | t.Fatalf("should have deleted log1") 256 | } 257 | if err := store.GetLog(2, new(raft.Log)); err != raft.ErrLogNotFound { 258 | t.Fatalf("should have deleted log2") 259 | } 260 | log3Dest := new(raft.Log) 261 | if err := store.GetLog(3, log3Dest); err != nil { 262 | t.Fatalf("should not have deleted log3") 263 | } 264 | if !reflect.DeepEqual(log3, log3Dest) { 265 | t.Fatalf("should not have deleted log3") 266 | } 267 | } 268 | 269 | func TestBadgerStore_Set_Get(t *testing.T) { 270 | store := testBadgerStore(t) 271 | defer store.Close() 272 | defer os.Remove(store.path) 273 | 274 | // Returns error on non-existent key 275 | if _, err := store.Get([]byte("bad")); err != ErrKeyNotFound { 276 | t.Fatalf("expected not found error, got: %q", err) 277 | } 278 | 279 | k, v := []byte("hello"), []byte("world") 280 | 281 | // Try to set a k/v pair 282 | if err := store.Set(k, v); err != nil { 283 | t.Fatalf("err: %s", err) 284 | } 285 | 286 | // Try to read it back 287 | val, err := store.Get(k) 288 | if err != nil { 289 | t.Fatalf("err: %s", err) 290 | } 291 | if !bytes.Equal(val, v) { 292 | t.Fatalf("bad: %v", val) 293 | } 294 | } 295 | 296 | func TestBadgerStore_SetUint64_GetUint64(t *testing.T) { 297 | store := testBadgerStore(t) 298 | defer store.Close() 299 | defer os.Remove(store.path) 300 | 301 | // Returns error on non-existent key 302 | if _, err := store.GetUint64([]byte("bad")); err != ErrKeyNotFound { 303 | t.Fatalf("expected not found error, got: %q", err) 304 | } 305 | 306 | k, v := []byte("abc"), uint64(123) 307 | 308 | // Attempt to set the k/v pair 309 | if err := store.SetUint64(k, v); err != nil { 310 | t.Fatalf("err: %s", err) 311 | } 312 | 313 | // Read back the value 314 | val, err := store.GetUint64(k) 315 | if err != nil { 316 | t.Fatalf("err: %s", err) 317 | } 318 | if val != v { 319 | t.Fatalf("bad: %v", val) 320 | } 321 | } 322 | 323 | func TestGenerateRanges(t *testing.T) { 324 | store := testBadgerStore(t) 325 | defer store.Close() 326 | defer os.Remove(store.path) 327 | maxSize := store.db.MaxBatchSize() 328 | testCases := []struct { 329 | desc string 330 | from int 331 | to int64 332 | expected int 333 | }{ 334 | { 335 | desc: "Single batch, at max", 336 | from: 0, 337 | to: maxSize, 338 | expected: 1, 339 | }, 340 | { 341 | desc: "Single batch, under max", 342 | from: 1.0, 343 | to: 10.0, 344 | expected: 1, 345 | }, 346 | { 347 | desc: "Multi batch, just above max", 348 | from: 0.0, 349 | to: maxSize + 1, 350 | expected: 2, 351 | }, 352 | { 353 | desc: "Multi batch, well above max", 354 | from: 0.0, 355 | to: 2*maxSize + 1, 356 | expected: 3, 357 | }, 358 | } 359 | for _, tC := range testCases { 360 | t.Run(tC.desc, func(t *testing.T) { 361 | ranges := store.generateRanges(uint64(tC.from), uint64(tC.to), maxSize) 362 | from := ranges[0].from 363 | to := ranges[len(ranges)-1].to 364 | if from != uint64(tC.from) || to != uint64(tC.to) { 365 | t.Log(ranges) 366 | t.Fatalf("err: range not covered. wanted to %v from %v ranges, got to: %v, from %v", tC.from, tC.to, from, to) 367 | } 368 | if len(ranges) != tC.expected { 369 | t.Fatalf("err: wanted %v ranges, got %v", tC.expected, len(ranges)) 370 | } 371 | }) 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package raftbadgerdb 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | raftbench "github.com/hashicorp/raft/bench" 8 | ) 9 | 10 | func BenchmarkBadgerStore_FirstIndex(b *testing.B) { 11 | store := testBadgerStore(b) 12 | defer store.Close() 13 | defer os.Remove(store.path) 14 | 15 | raftbench.FirstIndex(b, store) 16 | } 17 | 18 | func BenchmarkBadgerStore_LastIndex(b *testing.B) { 19 | store := testBadgerStore(b) 20 | defer store.Close() 21 | defer os.Remove(store.path) 22 | 23 | raftbench.LastIndex(b, store) 24 | } 25 | 26 | func BenchmarkBadgerStore_GetLog(b *testing.B) { 27 | store := testBadgerStore(b) 28 | defer store.Close() 29 | defer os.Remove(store.path) 30 | 31 | raftbench.GetLog(b, store) 32 | } 33 | 34 | func BenchmarkBadgerStore_StoreLog(b *testing.B) { 35 | store := testBadgerStore(b) 36 | defer store.Close() 37 | defer os.Remove(store.path) 38 | 39 | raftbench.StoreLog(b, store) 40 | } 41 | 42 | func BenchmarkBadgerStore_StoreLogs(b *testing.B) { 43 | store := testBadgerStore(b) 44 | defer store.Close() 45 | defer os.Remove(store.path) 46 | 47 | raftbench.StoreLogs(b, store) 48 | } 49 | 50 | func BenchmarkBadgerStore_DeleteRange(b *testing.B) { 51 | store := testBadgerStore(b) 52 | defer store.Close() 53 | defer os.Remove(store.path) 54 | raftbench.DeleteRange(b, store) 55 | } 56 | 57 | func BenchmarkBadgerStore_Set(b *testing.B) { 58 | store := testBadgerStore(b) 59 | defer store.Close() 60 | defer os.Remove(store.path) 61 | 62 | raftbench.Set(b, store) 63 | } 64 | 65 | func BenchmarkBadgerStore_Get(b *testing.B) { 66 | store := testBadgerStore(b) 67 | defer store.Close() 68 | defer os.Remove(store.path) 69 | 70 | raftbench.Get(b, store) 71 | } 72 | 73 | func BenchmarkBadgerStore_SetUint64(b *testing.B) { 74 | store := testBadgerStore(b) 75 | defer store.Close() 76 | defer os.Remove(store.path) 77 | 78 | raftbench.SetUint64(b, store) 79 | } 80 | 81 | func BenchmarkBadgerStore_GetUint64(b *testing.B) { 82 | store := testBadgerStore(b) 83 | defer store.Close() 84 | defer os.Remove(store.path) 85 | 86 | raftbench.GetUint64(b, store) 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/markthethomas/raft-badger 2 | 3 | require ( 4 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7 // indirect 5 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect 6 | github.com/dgraph-io/badger v1.5.4 7 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f // indirect 8 | github.com/golang/protobuf v1.2.0 // indirect 9 | github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 10 | github.com/hashicorp/go-msgpack v0.5.3 // indirect 11 | github.com/hashicorp/raft v1.0.0 12 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c // indirect 13 | github.com/pkg/errors v0.8.1 // indirect 14 | github.com/stretchr/testify v1.3.0 // indirect 15 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd // indirect 16 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7 h1:PqzgE6kAMi81xWQA2QIVxjWkFHptGgC547vchpUbtFo= 2 | github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 3 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= 4 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgraph-io/badger v1.5.4 h1:gVTrpUTbbr/T24uvoCaqY2KSHfNLVGm0w+hbee2HMeg= 8 | github.com/dgraph-io/badger v1.5.4/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= 9 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f h1:dDxpBYafY/GYpcl+LS4Bn3ziLPuEdGRkRjYAbSlWxSA= 10 | github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 11 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 14 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 15 | github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= 16 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 17 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 18 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 19 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 20 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 21 | github.com/hashicorp/raft v1.0.0 h1:htBVktAOtGs4Le5Z7K8SF5H2+oWsQFYVmOgH5loro7Y= 22 | github.com/hashicorp/raft v1.0.0/go.mod h1:DVSAWItjLjTOkVbSpWQ0j0kUADIvDaCtBxIcbNAQLkI= 23 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= 24 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 25 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 26 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 31 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 32 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= 33 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 35 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | --------------------------------------------------------------------------------