Compare commits

..

31 Commits

Author SHA1 Message Date
James Mills
d8a48f9eea Use pre-defined errors as they are comparable and useful as exported symbols 2019-03-20 07:39:03 +10:00
James Mills
65e7877bdf Remove notify step for now 2019-03-20 07:08:10 +10:00
James Mills
5711478dd6 Drone CI plugins now use a new key called settings :/ 2019-03-20 07:06:31 +10:00
James Mills
336795285e Fixed Drone CI config 2019-03-20 07:02:39 +10:00
James Mills
7fba9bd4b7 Add bitcaskd to install target 2019-03-20 07:00:46 +10:00
James Mills
e117ffd2e9 Fixed injecting Version/Commit in relased binaries 2019-03-19 18:55:03 +10:00
James Mills
ebefd0abf4 Updated deps 2019-03-18 19:47:08 +10:00
James Mills
52dfec6760 Only count coverage for the exported package 2019-03-18 17:40:45 +10:00
James Mills
1298240f53 Unexport some internal implemtnation details 2019-03-18 17:31:31 +10:00
James Mills
2a35976cdd Ooops 2019-03-17 14:00:15 +10:00
James Mills
6fe6fe0689 Refactored configuration option handling. Fixes #3 2019-03-17 13:53:30 +10:00
James Mills
e83608b903 Fixed missing error handling opening new Datafile(s) during Put() Fixes #4 2019-03-17 13:47:07 +10:00
James Mills
67ab944db7 Refactored some internals and removed timestamp field (unsure why it was needed in the original paper) 2019-03-16 12:40:24 +10:00
James Mills
cb00b11dd7 Increase no. of goroutines to catch more race conditions in tests 2019-03-16 12:33:07 +10:00
James Mills
e9c858d43f Add CRC Checksum checks on reading values back 2019-03-16 12:16:23 +10:00
James Mills
120e854444 Improved error messages 2019-03-16 11:47:22 +10:00
James Mills
d2f44d1513 Fix a race condition + Use my fork of trie 2019-03-16 11:22:55 +10:00
James Mills
c0f178c4f7 Improved read/write performance by another ~2x by not calling Stat() on every read/write 2019-03-16 08:15:07 +10:00
James Mills
2585222830 Improve write performance by ~33% to 80,000 writes/sec buf reducing syscalls and using a bufio.Writer 2019-03-16 07:41:37 +10:00
James Mills
3f1d6635c4 Add prefix scan for keys using a Trie 2019-03-15 23:48:50 +10:00
James Mills
67840ffb57 Call Close() at end of sub-commands 2019-03-14 21:50:41 +10:00
James Mills
9f0a357ca0 Remove lock file on Close() 2019-03-14 21:50:23 +10:00
James Mills
52b6c74a21 Fixed compile error in CLI 2019-03-14 21:33:40 +10:00
James Mills
d24a01797a Added WithMaxKeySize() and WithMaxValueSize() options 2019-03-14 21:31:23 +10:00
James Mills
bc8f6c6718 Change locking error message 2019-03-14 21:31:01 +10:00
James Mills
b6c212d60c Refactored option handling 2019-03-14 21:24:31 +10:00
James Mills
3f1b90eb23 Update README.md 2019-03-14 18:18:57 +10:00
James Mills
71a42800fe Improved benchmark test suite for various key/value sizes 2019-03-14 18:17:20 +10:00
James Mills
3b9627aeb8 Fix concurrent read bug 2019-03-14 17:58:06 +10:00
James Mills
e0c4c4fdae Fix concurrent write bug with multiple goroutines writing to the to the active datafile 2019-03-14 17:58:06 +10:00
James Mills
fb50eb2f82 Update README.md 2019-03-14 15:36:37 +10:00
27 changed files with 688 additions and 269 deletions

View File

@@ -5,18 +5,10 @@ steps:
- name: build - name: build
image: golang:latest image: golang:latest
commands: commands:
- go test -v -short -cover -coverprofile=coverage.txt ./... - go test -v -short -cover -coverprofile=coverage.txt -coverpkg=$(go list) ./...
- name: coverage - name: coverage
image: plugins/codecov image: plugins/codecov
settings: settings:
token: token:
from_secret: codecov-token from_secret: codecov-token
- name: notify
image: plugins/webhook
urls: https://msgbus.mills.io/ci.mills.io
when:
status:
- success
- failure

View File

@@ -3,14 +3,14 @@ builds:
binary: bitcask binary: bitcask
main: ./cmd/bitcask main: ./cmd/bitcask
flags: -tags "static_build" flags: -tags "static_build"
ldflags: -w -X .Version={{.Version}} -X .Commit={{.Commit}} ldflags: -w -X github.com/prologic/bitcask/internal.Version={{.Version}} -X github.com/prologic/bitcask/internal.Commit={{.Commit}}
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- -
binary: bitcaskd binary: bitcaskd
main: ./cmd/bitcaskd main: ./cmd/bitcaskd
flags: -tags "static_build" flags: -tags "static_build"
ldflags: -w -X .Version={{.Version}} -X .Commit={{.Commit}} ldflags: -w -X github.com/prologic/bitcask/internal.Version={{.Version}} -X github.com/prologic/bitcask/internal.Commit={{.Commit}}
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
sign: sign:

View File

@@ -13,11 +13,11 @@ dev: build
build: clean generate build: clean generate
@go build \ @go build \
-tags "netgo static_build" -installsuffix netgo \ -tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X $(shell go list).Version=$(VERSION) -X $(shell go list).Commit=$(COMMIT)" \ -ldflags "-w -X $(shell go list)/internal.Version=$(VERSION) -X $(shell go list)/internal.Commit=$(COMMIT)" \
./cmd/bitcask/... ./cmd/bitcask/...
@go build \ @go build \
-tags "netgo static_build" -installsuffix netgo \ -tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X $(shell go list).Version=$(VERSION) -X $(shell go list).Commit=$(COMMIT)" \ -ldflags "-w -X $(shell go list)/internal.Version=$(VERSION) -X $(shell go list)/internal.Commit=$(COMMIT)" \
./cmd/bitcaskd/... ./cmd/bitcaskd/...
generate: generate:
@@ -25,6 +25,7 @@ generate:
install: build install: build
@go install ./cmd/bitcask/... @go install ./cmd/bitcask/...
@go install ./cmd/bitcaskd/...
image: image:
@docker build -t prologic/bitcask . @docker build -t prologic/bitcask .
@@ -39,7 +40,7 @@ bench: build
@go test -v -benchmem -bench=. ./... @go test -v -benchmem -bench=. ./...
test: build test: build
@go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./... @go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=$(shell go list) -race ./...
clean: clean:
@git clean -f -d -X @git clean -f -d -X

View File

@@ -12,6 +12,7 @@ A Bitcask (LSM+WAL) Key/Value Store written in Go.
* Embeddable * Embeddable
* Builtin CLI * Builtin CLI
* Builtin Redis-compatible server
* Predictable read/write performance * Predictable read/write performance
* Low latecny * Low latecny
* High throughput (See: [Performance](README.md#Performance) * High throughput (See: [Performance](README.md#Performance)
@@ -96,12 +97,35 @@ Benchmarks run on a 11" Macbook with a 1.4Ghz Intel Core i7:
``` ```
$ make bench $ make bench
... ...
BenchmarkGet-4 300000 5065 ns/op 144 B/op 4 allocs/op BenchmarkGet/128B-4 300000 5178 ns/op 400 B/op 5 allocs/op
BenchmarkPut-4 100000 14640 ns/op 699 B/op 7 allocs/op BenchmarkGet/256B-4 300000 5273 ns/op 656 B/op 5 allocs/op
BenchmarkGet/512B-4 200000 5368 ns/op 1200 B/op 5 allocs/op
BenchmarkGet/1K-4 200000 5800 ns/op 2288 B/op 5 allocs/op
BenchmarkGet/2K-4 200000 6766 ns/op 4464 B/op 5 allocs/op
BenchmarkGet/4K-4 200000 7857 ns/op 9072 B/op 5 allocs/op
BenchmarkGet/8K-4 200000 9538 ns/op 17776 B/op 5 allocs/op
BenchmarkGet/16K-4 100000 13188 ns/op 34928 B/op 5 allocs/op
BenchmarkGet/32K-4 100000 21620 ns/op 73840 B/op 5 allocs/op
BenchmarkPut/128B-4 200000 7875 ns/op 409 B/op 6 allocs/op
BenchmarkPut/256B-4 200000 8712 ns/op 538 B/op 6 allocs/op
BenchmarkPut/512B-4 200000 9832 ns/op 829 B/op 6 allocs/op
BenchmarkPut/1K-4 100000 13105 ns/op 1410 B/op 6 allocs/op
BenchmarkPut/2K-4 100000 18601 ns/op 2572 B/op 6 allocs/op
BenchmarkPut/4K-4 50000 36631 ns/op 5151 B/op 6 allocs/op
BenchmarkPut/8K-4 30000 56128 ns/op 9798 B/op 6 allocs/op
BenchmarkPut/16K-4 20000 83209 ns/op 18834 B/op 6 allocs/op
BenchmarkPut/32K-4 10000 135899 ns/op 41517 B/op 6 allocs/op
BenchmarkScan-4 1000000 1851 ns/op 493 B/op 25 allocs/op
``` ```
* ~180,000 reads/sec For 128B values:
* ~60,000 writes/sec
* ~200,000 reads/sec
* ~130,000 writes/sec
The full benchmark above shows linear performance as you increase key/value sizes.
## License ## License

View File

@@ -2,35 +2,36 @@ package bitcask
import ( import (
"errors" "errors"
"fmt" "hash/crc32"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv"
"strings" "strings"
"time"
"github.com/gofrs/flock" "github.com/gofrs/flock"
) "github.com/prologic/trie"
const ( "github.com/prologic/bitcask/internal"
DefaultMaxDatafileSize = 1 << 20 // 1MB
) )
var ( var (
ErrKeyNotFound = errors.New("error: key not found") ErrKeyNotFound = errors.New("error: key not found")
ErrCannotAcquireLock = errors.New("error: cannot acquire lock") ErrKeyTooLarge = errors.New("error: key too large")
ErrValueTooLarge = errors.New("error: value too large")
ErrChecksumFailed = errors.New("error: checksum failed")
ErrDatabaseLocked = errors.New("error: database locked")
) )
type Bitcask struct { type Bitcask struct {
*flock.Flock *flock.Flock
config *config
path string path string
curr *Datafile curr *internal.Datafile
keydir *Keydir keydir *internal.Keydir
datafiles []*Datafile datafiles []*internal.Datafile
trie *trie.Trie
maxDatafileSize int64 maxDatafileSize int64
} }
@@ -38,6 +39,7 @@ type Bitcask struct {
func (b *Bitcask) Close() error { func (b *Bitcask) Close() error {
defer func() { defer func() {
b.Flock.Unlock() b.Flock.Unlock()
os.Remove(b.Flock.Path())
}() }()
for _, df := range b.datafiles { for _, df := range b.datafiles {
@@ -51,34 +53,47 @@ func (b *Bitcask) Sync() error {
} }
func (b *Bitcask) Get(key string) ([]byte, error) { func (b *Bitcask) Get(key string) ([]byte, error) {
var df *Datafile var df *internal.Datafile
item, ok := b.keydir.Get(key) item, ok := b.keydir.Get(key)
if !ok { if !ok {
return nil, ErrKeyNotFound return nil, ErrKeyNotFound
} }
if item.FileID == b.curr.id { if item.FileID == b.curr.FileID() {
df = b.curr df = b.curr
} else { } else {
df = b.datafiles[item.FileID] df = b.datafiles[item.FileID]
} }
e, err := df.ReadAt(item.Index) e, err := df.ReadAt(item.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
checksum := crc32.ChecksumIEEE(e.Value)
if checksum != e.Checksum {
return nil, ErrChecksumFailed
}
return e.Value, nil return e.Value, nil
} }
func (b *Bitcask) Put(key string, value []byte) error { func (b *Bitcask) Put(key string, value []byte) error {
index, err := b.put(key, value) if len(key) > b.config.MaxKeySize {
return ErrKeyTooLarge
}
if len(value) > b.config.MaxValueSize {
return ErrValueTooLarge
}
offset, err := b.put(key, value)
if err != nil { if err != nil {
return err return err
} }
b.keydir.Add(key, b.curr.id, index, time.Now().Unix()) item := b.keydir.Add(key, b.curr.FileID(), offset)
b.trie.Add(key, item)
return nil return nil
} }
@@ -90,10 +105,21 @@ func (b *Bitcask) Delete(key string) error {
} }
b.keydir.Delete(key) b.keydir.Delete(key)
b.trie.Remove(key)
return nil return nil
} }
func (b *Bitcask) Scan(prefix string, f func(key string) error) error {
keys := b.trie.PrefixSearch(prefix)
for _, key := range keys {
if err := f(key); err != nil {
return err
}
}
return nil
}
func (b *Bitcask) Fold(f func(key string) error) error { func (b *Bitcask) Fold(f func(key string) error) error {
for key := range b.keydir.Keys() { for key := range b.keydir.Keys() {
if err := f(key); err != nil { if err := f(key); err != nil {
@@ -115,18 +141,22 @@ func (b *Bitcask) put(key string, value []byte) (int64, error) {
return -1, err return -1, err
} }
df, err := NewDatafile(b.path, b.curr.id, true) df, err := internal.NewDatafile(b.path, b.curr.FileID(), true)
if err != nil {
return -1, err
}
b.datafiles = append(b.datafiles, df) b.datafiles = append(b.datafiles, df)
id := b.curr.id + 1 id := b.curr.FileID() + 1
curr, err := NewDatafile(b.path, id, false) curr, err := internal.NewDatafile(b.path, id, false)
if err != nil { if err != nil {
return -1, err return -1, err
} }
b.curr = curr b.curr = curr
} }
e := NewEntry(key, value) e := internal.NewEntry(key, value)
return b.curr.Write(e) return b.curr.Write(e)
} }
@@ -135,46 +165,13 @@ func (b *Bitcask) setMaxDatafileSize(size int64) error {
return nil return nil
} }
func WithMaxDatafileSize(size int64) func(*Bitcask) error {
return func(b *Bitcask) error {
return b.setMaxDatafileSize(size)
}
}
func getDatafiles(path string) ([]string, error) {
fns, err := filepath.Glob(fmt.Sprintf("%s/*.data", path))
if err != nil {
return nil, err
}
sort.Strings(fns)
return fns, nil
}
func parseIds(fns []string) ([]int, error) {
var ids []int
for _, fn := range fns {
fn = filepath.Base(fn)
ext := filepath.Ext(fn)
if ext != ".data" {
continue
}
id, err := strconv.ParseInt(strings.TrimSuffix(fn, ext), 10, 32)
if err != nil {
return nil, err
}
ids = append(ids, int(id))
}
sort.Ints(ids)
return ids, nil
}
func Merge(path string, force bool) error { func Merge(path string, force bool) error {
fns, err := getDatafiles(path) fns, err := internal.GetDatafiles(path)
if err != nil { if err != nil {
return err return err
} }
ids, err := parseIds(fns) ids, err := internal.ParseIds(fns)
if err != nil { if err != nil {
return err return err
} }
@@ -204,9 +201,9 @@ func Merge(path string, force bool) error {
id := ids[i] id := ids[i]
keydir := NewKeydir() keydir := internal.NewKeydir()
df, err := NewDatafile(path, id, true) df, err := internal.NewDatafile(path, id, true)
if err != nil { if err != nil {
return err return err
} }
@@ -227,10 +224,10 @@ func Merge(path string, force bool) error {
continue continue
} }
keydir.Add(e.Key, ids[i], e.Index, e.Timestamp) keydir.Add(e.Key, ids[i], e.Offset)
} }
tempdf, err := NewDatafile(temp, id, false) tempdf, err := internal.NewDatafile(temp, id, false)
if err != nil { if err != nil {
return err return err
} }
@@ -238,7 +235,7 @@ func Merge(path string, force bool) error {
for key := range keydir.Keys() { for key := range keydir.Keys() {
item, _ := keydir.Get(key) item, _ := keydir.Get(key)
e, err := df.ReadAt(item.Index) e, err := df.ReadAt(item.Offset)
if err != nil { if err != nil {
return err return err
} }
@@ -274,7 +271,7 @@ func Merge(path string, force bool) error {
return nil return nil
} }
func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) { func Open(path string, options ...option) (*Bitcask, error) {
if err := os.MkdirAll(path, 0755); err != nil { if err := os.MkdirAll(path, 0755); err != nil {
return nil, err return nil, err
} }
@@ -284,21 +281,23 @@ func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) {
return nil, err return nil, err
} }
fns, err := getDatafiles(path) fns, err := internal.GetDatafiles(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ids, err := parseIds(fns) ids, err := internal.ParseIds(fns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
keydir := NewKeydir() var datafiles []*internal.Datafile
var datafiles []*Datafile
keydir := internal.NewKeydir()
trie := trie.New()
for i, fn := range fns { for i, fn := range fns {
df, err := NewDatafile(path, ids[i], true) df, err := internal.NewDatafile(path, ids[i], true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -311,14 +310,15 @@ func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) {
} }
defer f.Close() defer f.Close()
hint, err := NewKeydirFromBytes(f) hint, err := internal.NewKeydirFromBytes(f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for key := range hint.Keys() { for key := range hint.Keys() {
item, _ := hint.Get(key) item, _ := hint.Get(key)
keydir.Add(key, item.FileID, item.Index, item.Timestamp) _ = keydir.Add(key, item.FileID, item.Offset)
trie.Add(key, item)
} }
} else { } else {
for { for {
@@ -336,7 +336,8 @@ func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) {
continue continue
} }
keydir.Add(e.Key, ids[i], e.Index, e.Timestamp) item := keydir.Add(e.Key, ids[i], e.Offset)
trie.Add(e.Key, item)
} }
} }
} }
@@ -346,23 +347,25 @@ func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) {
id = ids[(len(ids) - 1)] id = ids[(len(ids) - 1)]
} }
curr, err := NewDatafile(path, id, false) curr, err := internal.NewDatafile(path, id, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
bitcask := &Bitcask{ bitcask := &Bitcask{
Flock: flock.New(filepath.Join(path, "lock")), Flock: flock.New(filepath.Join(path, "lock")),
config: NewDefaultConfig(),
path: path, path: path,
curr: curr, curr: curr,
keydir: keydir, keydir: keydir,
datafiles: datafiles, datafiles: datafiles,
trie: trie,
maxDatafileSize: DefaultMaxDatafileSize, maxDatafileSize: DefaultMaxDatafileSize,
} }
for _, option := range options { for _, opt := range options {
err = option(bitcask) err = opt(bitcask.config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -374,7 +377,7 @@ func Open(path string, options ...func(*Bitcask) error) (*Bitcask, error) {
} }
if !locked { if !locked {
return nil, ErrCannotAcquireLock return nil, ErrDatabaseLocked
} }
return bitcask, nil return bitcask, nil

View File

@@ -3,7 +3,10 @@ package bitcask
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"reflect"
"sort"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -42,7 +45,7 @@ func TestAll(t *testing.T) {
assert.NoError(err) assert.NoError(err)
_, err = db.Get("foo") _, err = db.Get("foo")
assert.Error(err) assert.Error(err)
assert.Equal(err.Error(), "error: key not found") assert.Equal(ErrKeyNotFound, err)
}) })
t.Run("Sync", func(t *testing.T) { t.Run("Sync", func(t *testing.T) {
@@ -89,7 +92,7 @@ func TestDeletedKeys(t *testing.T) {
assert.NoError(err) assert.NoError(err)
_, err = db.Get("foo") _, err = db.Get("foo")
assert.Error(err) assert.Error(err)
assert.Equal("error: key not found", err.Error()) assert.Equal(ErrKeyNotFound, err)
}) })
t.Run("Sync", func(t *testing.T) { t.Run("Sync", func(t *testing.T) {
@@ -117,7 +120,7 @@ func TestDeletedKeys(t *testing.T) {
t.Run("Get", func(t *testing.T) { t.Run("Get", func(t *testing.T) {
_, err = db.Get("foo") _, err = db.Get("foo")
assert.Error(err) assert.Error(err)
assert.Equal("error: key not found", err.Error()) assert.Equal(ErrKeyNotFound, err)
}) })
t.Run("Close", func(t *testing.T) { t.Run("Close", func(t *testing.T) {
@@ -127,6 +130,50 @@ func TestDeletedKeys(t *testing.T) {
}) })
} }
func TestMaxKeySize(t *testing.T) {
assert := assert.New(t)
testdir, err := ioutil.TempDir("", "bitcask")
assert.NoError(err)
var db *Bitcask
t.Run("Open", func(t *testing.T) {
db, err = Open(testdir, WithMaxKeySize(16))
assert.NoError(err)
})
t.Run("Put", func(t *testing.T) {
key := strings.Repeat(" ", 17)
value := []byte("foobar")
err = db.Put(key, value)
assert.Error(err)
assert.Equal(ErrKeyTooLarge, err)
})
}
func TestMaxValueSize(t *testing.T) {
assert := assert.New(t)
testdir, err := ioutil.TempDir("", "bitcask")
assert.NoError(err)
var db *Bitcask
t.Run("Open", func(t *testing.T) {
db, err = Open(testdir, WithMaxValueSize(16))
assert.NoError(err)
})
t.Run("Put", func(t *testing.T) {
key := "foo"
value := []byte(strings.Repeat(" ", 17))
err = db.Put(key, value)
assert.Error(err)
assert.Equal(ErrValueTooLarge, err)
})
}
func TestMerge(t *testing.T) { func TestMerge(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
@@ -198,6 +245,136 @@ func TestMerge(t *testing.T) {
}) })
} }
func TestConcurrent(t *testing.T) {
var (
db *Bitcask
err error
)
assert := assert.New(t)
testdir, err := ioutil.TempDir("", "bitcask")
assert.NoError(err)
t.Run("Setup", func(t *testing.T) {
t.Run("Open", func(t *testing.T) {
db, err = Open(testdir)
assert.NoError(err)
})
t.Run("Put", func(t *testing.T) {
err = db.Put("foo", []byte("bar"))
assert.NoError(err)
})
})
t.Run("Concurrent", func(t *testing.T) {
t.Run("Put", func(t *testing.T) {
f := func(wg *sync.WaitGroup, x int) {
defer func() {
wg.Done()
}()
for i := 0; i <= 100; i++ {
if i%x == 0 {
key := fmt.Sprintf("k%d", i)
value := []byte(fmt.Sprintf("v%d", i))
err := db.Put(key, value)
assert.NoError(err)
}
}
}
wg := &sync.WaitGroup{}
go f(wg, 2)
go f(wg, 3)
go f(wg, 5)
wg.Add(3)
wg.Wait()
})
t.Run("Get", func(t *testing.T) {
f := func(wg *sync.WaitGroup, N int) {
defer func() {
wg.Done()
}()
for i := 0; i <= N; i++ {
value, err := db.Get("foo")
assert.NoError(err)
assert.Equal([]byte("bar"), value)
}
}
wg := &sync.WaitGroup{}
go f(wg, 100)
go f(wg, 100)
go f(wg, 100)
wg.Add(3)
wg.Wait()
})
t.Run("Close", func(t *testing.T) {
err = db.Close()
assert.NoError(err)
})
})
}
func TestScan(t *testing.T) {
assert := assert.New(t)
testdir, err := ioutil.TempDir("", "bitcask")
assert.NoError(err)
var db *Bitcask
t.Run("Setup", func(t *testing.T) {
t.Run("Open", func(t *testing.T) {
db, err = Open(testdir)
assert.NoError(err)
})
t.Run("Put", func(t *testing.T) {
var items = map[string][]byte{
"1": []byte("1"),
"2": []byte("2"),
"3": []byte("3"),
"food": []byte("pizza"),
"foo": []byte("foo"),
"fooz": []byte("fooz ball"),
"hello": []byte("world"),
}
for k, v := range items {
err = db.Put(k, v)
assert.NoError(err)
}
})
})
t.Run("Scan", func(t *testing.T) {
var (
vals []string
expected = []string{
"foo",
"fooz ball",
"pizza",
}
)
err = db.Scan("fo", func(key string) error {
val, err := db.Get(key)
assert.NoError(err)
vals = append(vals, string(val))
return nil
})
sort.Strings(vals)
assert.Equal(expected, vals)
})
}
func TestLocking(t *testing.T) { func TestLocking(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
@@ -210,7 +387,12 @@ func TestLocking(t *testing.T) {
_, err = Open(testdir) _, err = Open(testdir)
assert.Error(err) assert.Error(err)
assert.Equal("error: cannot acquire lock", err.Error()) assert.Equal(ErrDatabaseLocked, err)
}
type benchmarkTestCase struct {
name string
size int
} }
func BenchmarkGet(b *testing.B) { func BenchmarkGet(b *testing.B) {
@@ -225,20 +407,39 @@ func BenchmarkGet(b *testing.B) {
} }
defer db.Close() defer db.Close()
err = db.Put("foo", []byte("bar")) tests := []benchmarkTestCase{
if err != nil { {"128B", 128},
b.Fatal(err) {"256B", 256},
{"512B", 512},
{"1K", 1024},
{"2K", 2048},
{"4K", 4096},
{"8K", 8192},
{"16K", 16384},
{"32K", 32768},
} }
b.ResetTimer() for _, tt := range tests {
for i := 0; i < b.N; i++ { b.Run(tt.name, func(b *testing.B) {
val, err := db.Get("foo") key := "foo"
if err != nil { value := []byte(strings.Repeat(" ", tt.size))
b.Fatal(err)
} err = db.Put(key, value)
if string(val) != "bar" { if err != nil {
b.Errorf("expected val=bar got=%s", val) b.Fatal(err)
} }
b.ResetTimer()
for i := 0; i < b.N; i++ {
val, err := db.Get(key)
if err != nil {
b.Fatal(err)
}
if string(val) != string(value) {
b.Errorf("unexpected value")
}
}
})
} }
} }
@@ -254,11 +455,73 @@ func BenchmarkPut(b *testing.B) {
} }
defer db.Close() defer db.Close()
b.ResetTimer() tests := []benchmarkTestCase{
for i := 0; i < b.N; i++ { {"128B", 128},
err := db.Put(fmt.Sprintf("key%d", i), []byte("bar")) {"256B", 256},
{"512B", 512},
{"1K", 1024},
{"2K", 2048},
{"4K", 4096},
{"8K", 8192},
{"16K", 16384},
{"32K", 32768},
}
for _, tt := range tests {
b.Run(tt.name, func(b *testing.B) {
key := "foo"
value := []byte(strings.Repeat(" ", tt.size))
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := db.Put(key, value)
if err != nil {
b.Fatal(err)
}
}
})
}
}
func BenchmarkScan(b *testing.B) {
testdir, err := ioutil.TempDir("", "bitcask")
if err != nil {
b.Fatal(err)
}
db, err := Open(testdir)
if err != nil {
b.Fatal(err)
}
defer db.Close()
var items = map[string][]byte{
"1": []byte("1"),
"2": []byte("2"),
"3": []byte("3"),
"food": []byte("pizza"),
"foo": []byte("foo"),
"fooz": []byte("fooz ball"),
"hello": []byte("world"),
}
for k, v := range items {
err := db.Put(k, v)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
var expected = []string{"foo", "food", "fooz"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var keys []string
err = db.Scan("fo", func(key string) error {
keys = append(keys, key)
return nil
})
sort.Strings(keys)
if !reflect.DeepEqual(expected, keys) {
b.Fatal(fmt.Errorf("expected keys=#%v got=%#v", expected, keys))
}
}
} }

View File

@@ -35,6 +35,7 @@ func del(path, key string) int {
log.WithError(err).Error("error opening database") log.WithError(err).Error("error opening database")
return 1 return 1
} }
defer db.Close()
err = db.Delete(key) err = db.Delete(key)
if err != nil { if err != nil {

View File

@@ -36,6 +36,7 @@ func get(path, key string) int {
log.WithError(err).Error("error opening database") log.WithError(err).Error("error opening database")
return 1 return 1
} }
defer db.Close()
value, err := db.Get(key) value, err := db.Get(key)
if err != nil { if err != nil {

View File

@@ -34,6 +34,7 @@ func keys(path string) int {
log.WithError(err).Error("error opening database") log.WithError(err).Error("error opening database")
return 1 return 1
} }
defer db.Close()
err = db.Fold(func(key string) error { err = db.Fold(func(key string) error {
fmt.Printf("%s\n", key) fmt.Printf("%s\n", key)

View File

@@ -8,13 +8,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/prologic/bitcask" "github.com/prologic/bitcask/internal"
) )
// RootCmd represents the base command when called without any subcommands // RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "bitcask", Use: "bitcask",
Version: bitcask.FullVersion(), Version: internal.FullVersion(),
Short: "Command-line tools for bitcask", Short: "Command-line tools for bitcask",
Long: `This is the command-line tool to interact with a bitcask database. Long: `This is the command-line tool to interact with a bitcask database.

60
cmd/bitcask/scan.go Normal file
View File

@@ -0,0 +1,60 @@
package main
import (
"fmt"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/prologic/bitcask"
)
var scanCmd = &cobra.Command{
Use: "scan <prefix>",
Aliases: []string{"search", "find"},
Short: "Perform a prefis scan for keys",
Long: `This performa a prefix scan for keys starting with the given
prefix. This uses a Trie to search for matching keys and returns all matched
keys.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
path := viper.GetString("path")
prefix := args[0]
os.Exit(scan(path, prefix))
},
}
func init() {
RootCmd.AddCommand(scanCmd)
}
func scan(path, prefix string) int {
db, err := bitcask.Open(path)
if err != nil {
log.WithError(err).Error("error opening database")
return 1
}
defer db.Close()
err = db.Scan(prefix, func(key string) error {
value, err := db.Get(key)
if err != nil {
log.WithError(err).Error("error reading key")
return err
}
fmt.Printf("%s\n", string(value))
log.WithField("key", key).WithField("value", value).Debug("key/value")
return nil
})
if err != nil {
log.WithError(err).Error("error scanning keys")
return 1
}
return 0
}

View File

@@ -47,6 +47,7 @@ func set(path, key string, value io.Reader) int {
log.WithError(err).Error("error opening database") log.WithError(err).Error("error opening database")
return 1 return 1
} }
defer db.Close()
data, err := ioutil.ReadAll(value) data, err := ioutil.ReadAll(value)
if err != nil { if err != nil {

View File

@@ -10,13 +10,14 @@ import (
"github.com/tidwall/redcon" "github.com/tidwall/redcon"
"github.com/prologic/bitcask" "github.com/prologic/bitcask"
"github.com/prologic/bitcask/internal"
) )
var ( var (
bind string bind string
debug bool debug bool
version bool version bool
maxDatafileSize int64 maxDatafileSize int
) )
func init() { func init() {
@@ -30,7 +31,7 @@ func init() {
flag.StringVarP(&bind, "bind", "b", ":6379", "interface and port to bind to") flag.StringVarP(&bind, "bind", "b", ":6379", "interface and port to bind to")
flag.Int64Var(&maxDatafileSize, "max-datafile-size", 1<<20, "maximum datafile size in bytes") flag.IntVar(&maxDatafileSize, "max-datafile-size", 1<<20, "maximum datafile size in bytes")
} }
func main() { func main() {
@@ -43,7 +44,7 @@ func main() {
} }
if version { if version {
fmt.Printf("bitcaskd version %s", bitcask.FullVersion()) fmt.Printf("bitcaskd version %s", internal.FullVersion())
os.Exit(0) os.Exit(0)
} }
@@ -60,7 +61,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
log.WithField("bind", bind).WithField("path", path).Infof("starting bitcaskd v%s", bitcask.FullVersion()) log.WithField("bind", bind).WithField("path", path).Infof("starting bitcaskd v%s", internal.FullVersion())
err = redcon.ListenAndServe(bind, err = redcon.ListenAndServe(bind,
func(conn redcon.Conn, cmd redcon.Command) { func(conn redcon.Conn, cmd redcon.Command) {

View File

@@ -1,17 +0,0 @@
package bitcask
import (
"hash/crc32"
pb "github.com/prologic/bitcask/proto"
)
func NewEntry(key string, value []byte) pb.Entry {
crc := crc32.ChecksumIEEE(value)
return pb.Entry{
CRC: crc,
Key: key,
Value: value,
}
}

10
go.mod
View File

@@ -1,20 +1,18 @@
module github.com/prologic/bitcask module github.com/prologic/bitcask
require ( require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/gofrs/flock v0.7.1 github.com/gofrs/flock v0.7.1
github.com/gogo/protobuf v1.2.1 github.com/gogo/protobuf v1.2.1
github.com/golang/protobuf v1.2.0 github.com/golang/protobuf v1.2.0
github.com/gorilla/websocket v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/prologic/msgbus v0.1.1 github.com/prologic/trie v0.0.0-20190316011403-395e39dac705
github.com/prometheus/client_golang v0.9.2 // indirect
github.com/sirupsen/logrus v1.3.0 github.com/sirupsen/logrus v1.3.0
github.com/spf13/cobra v0.0.3 github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.1 github.com/spf13/viper v1.3.1
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/tidwall/redcon v0.9.0 github.com/tidwall/redcon v0.9.0
gopkg.in/vmihailenco/msgpack.v2 v2.9.1 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
) )

31
go.sum
View File

@@ -1,6 +1,6 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -16,21 +16,16 @@ github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
@@ -39,15 +34,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prologic/msgbus v0.1.1/go.mod h1:B3Qu4/U2FP08x93jUzp9E8bl155+cIgDH2DUGRK6OZk= github.com/prologic/trie v0.0.0-20190316011403-395e39dac705 h1:2J+cSlAeECj0lfMKSmM7n5OlIio+yLovaKLZJzwLc6U=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= github.com/prologic/trie v0.0.0-20190316011403-395e39dac705/go.mod h1:LFuDmpHJGmciXd8Rl5YMhVlLMps9gz2GtYLzwxrFhzs=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
@@ -74,7 +62,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
@@ -82,8 +70,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/vmihailenco/msgpack.v2 v2.9.1 h1:kb0VV7NuIojvRfzwslQeP3yArBqJHW9tOl4t38VS1jM=
gopkg.in/vmihailenco/msgpack.v2 v2.9.1/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -1,14 +1,15 @@
package bitcask package internal
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time" "sync"
pb "github.com/prologic/bitcask/proto" "github.com/pkg/errors"
"github.com/prologic/bitcask/streampb"
pb "github.com/prologic/bitcask/internal/proto"
"github.com/prologic/bitcask/internal/streampb"
) )
const ( const (
@@ -20,11 +21,14 @@ var (
) )
type Datafile struct { type Datafile struct {
id int sync.RWMutex
r *os.File
w *os.File id int
dec *streampb.Decoder r *os.File
enc *streampb.Encoder w *os.File
offset int64
dec *streampb.Decoder
enc *streampb.Encoder
} }
func NewDatafile(path string, id int, readonly bool) (*Datafile, error) { func NewDatafile(path string, id int, readonly bool) (*Datafile, error) {
@@ -47,19 +51,30 @@ func NewDatafile(path string, id int, readonly bool) (*Datafile, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
stat, err := r.Stat()
if err != nil {
return nil, errors.Wrap(err, "error calling Stat()")
}
offset := stat.Size()
dec := streampb.NewDecoder(r) dec := streampb.NewDecoder(r)
enc := streampb.NewEncoder(w) enc := streampb.NewEncoder(w)
return &Datafile{ return &Datafile{
id: id, id: id,
r: r, r: r,
w: w, w: w,
dec: dec, offset: offset,
enc: enc, dec: dec,
enc: enc,
}, nil }, nil
} }
func (df *Datafile) FileID() int {
return df.id
}
func (df *Datafile) Name() string { func (df *Datafile) Name() string {
return df.r.Name() return df.r.Name()
} }
@@ -84,35 +99,28 @@ func (df *Datafile) Sync() error {
} }
func (df *Datafile) Size() (int64, error) { func (df *Datafile) Size() (int64, error) {
var ( df.RLock()
stat os.FileInfo defer df.RUnlock()
err error return df.offset, nil
)
if df.w == nil {
stat, err = df.r.Stat()
} else {
stat, err = df.w.Stat()
}
if err != nil {
return -1, err
}
return stat.Size(), nil
} }
func (df *Datafile) Read() (pb.Entry, error) { func (df *Datafile) Read() (e pb.Entry, err error) {
var e pb.Entry df.Lock()
defer df.Unlock()
return e, df.dec.Decode(&e) return e, df.dec.Decode(&e)
} }
func (df *Datafile) ReadAt(index int64) (e pb.Entry, err error) { func (df *Datafile) ReadAt(index int64) (e pb.Entry, err error) {
df.Lock()
defer df.Unlock()
_, err = df.r.Seek(index, os.SEEK_SET) _, err = df.r.Seek(index, os.SEEK_SET)
if err != nil { if err != nil {
return return
} }
return df.Read()
return e, df.dec.Decode(&e)
} }
func (df *Datafile) Write(e pb.Entry) (int64, error) { func (df *Datafile) Write(e pb.Entry) (int64, error) {
@@ -120,20 +128,16 @@ func (df *Datafile) Write(e pb.Entry) (int64, error) {
return -1, ErrReadonly return -1, ErrReadonly
} }
stat, err := df.w.Stat() df.Lock()
defer df.Unlock()
e.Offset = df.offset
n, err := df.enc.Encode(&e)
if err != nil { if err != nil {
return -1, err return -1, err
} }
df.offset += n
index := stat.Size() return e.Offset, nil
e.Index = index
e.Timestamp = time.Now().Unix()
err = df.enc.Encode(&e)
if err != nil {
return -1, err
}
return index, nil
} }

17
internal/entry.go Normal file
View File

@@ -0,0 +1,17 @@
package internal
import (
"hash/crc32"
pb "github.com/prologic/bitcask/internal/proto"
)
func NewEntry(key string, value []byte) pb.Entry {
checksum := crc32.ChecksumIEEE(value)
return pb.Entry{
Checksum: checksum,
Key: key,
Value: value,
}
}

View File

@@ -1,4 +1,4 @@
package bitcask package internal
import ( import (
"bytes" "bytes"
@@ -9,9 +9,8 @@ import (
) )
type Item struct { type Item struct {
FileID int FileID int
Index int64 Offset int64
Timestamp int64
} }
type Keydir struct { type Keydir struct {
@@ -25,15 +24,17 @@ func NewKeydir() *Keydir {
} }
} }
func (k *Keydir) Add(key string, fileid int, index, timestamp int64) { func (k *Keydir) Add(key string, fileid int, offset int64) Item {
k.Lock() item := Item{
defer k.Unlock() FileID: fileid,
Offset: offset,
k.kv[key] = Item{
FileID: fileid,
Index: index,
Timestamp: timestamp,
} }
k.Lock()
k.kv[key] = item
k.Unlock()
return item
} }
func (k *Keydir) Get(key string) (Item, bool) { func (k *Keydir) Get(key string) (Item, bool) {

View File

@@ -19,11 +19,10 @@ var _ = math.Inf
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Entry struct { type Entry struct {
CRC uint32 `protobuf:"varint,1,opt,name=CRC,proto3" json:"CRC,omitempty"` Checksum uint32 `protobuf:"varint,1,opt,name=Checksum,proto3" json:"Checksum,omitempty"`
Key string `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"` Key string `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"`
Index int64 `protobuf:"varint,3,opt,name=Index,proto3" json:"Index,omitempty"` Offset int64 `protobuf:"varint,3,opt,name=Offset,proto3" json:"Offset,omitempty"`
Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"`
Timestamp int64 `protobuf:"varint,5,opt,name=Timestamp,proto3" json:"Timestamp,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@@ -33,7 +32,7 @@ func (m *Entry) Reset() { *m = Entry{} }
func (m *Entry) String() string { return proto.CompactTextString(m) } func (m *Entry) String() string { return proto.CompactTextString(m) }
func (*Entry) ProtoMessage() {} func (*Entry) ProtoMessage() {}
func (*Entry) Descriptor() ([]byte, []int) { func (*Entry) Descriptor() ([]byte, []int) {
return fileDescriptor_entry_4f5906245d08394f, []int{0} return fileDescriptor_entry_3e91842c99935ae2, []int{0}
} }
func (m *Entry) XXX_Unmarshal(b []byte) error { func (m *Entry) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Entry.Unmarshal(m, b) return xxx_messageInfo_Entry.Unmarshal(m, b)
@@ -53,9 +52,9 @@ func (m *Entry) XXX_DiscardUnknown() {
var xxx_messageInfo_Entry proto.InternalMessageInfo var xxx_messageInfo_Entry proto.InternalMessageInfo
func (m *Entry) GetCRC() uint32 { func (m *Entry) GetChecksum() uint32 {
if m != nil { if m != nil {
return m.CRC return m.Checksum
} }
return 0 return 0
} }
@@ -67,9 +66,9 @@ func (m *Entry) GetKey() string {
return "" return ""
} }
func (m *Entry) GetIndex() int64 { func (m *Entry) GetOffset() int64 {
if m != nil { if m != nil {
return m.Index return m.Offset
} }
return 0 return 0
} }
@@ -81,28 +80,20 @@ func (m *Entry) GetValue() []byte {
return nil return nil
} }
func (m *Entry) GetTimestamp() int64 {
if m != nil {
return m.Timestamp
}
return 0
}
func init() { func init() {
proto.RegisterType((*Entry)(nil), "proto.Entry") proto.RegisterType((*Entry)(nil), "proto.Entry")
} }
func init() { proto.RegisterFile("entry.proto", fileDescriptor_entry_4f5906245d08394f) } func init() { proto.RegisterFile("entry.proto", fileDescriptor_entry_3e91842c99935ae2) }
var fileDescriptor_entry_4f5906245d08394f = []byte{ var fileDescriptor_entry_3e91842c99935ae2 = []byte{
// 134 bytes of a gzipped FileDescriptorProto // 126 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4e, 0xcd, 0x2b, 0x29, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4e, 0xcd, 0x2b, 0x29,
0xaa, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0xa5, 0x5c, 0xac, 0xae, 0xaa, 0xd4, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0xc9, 0x5c, 0xac, 0xae,
0x20, 0x51, 0x21, 0x01, 0x2e, 0x66, 0xe7, 0x20, 0x67, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xde, 0x20, 0x20, 0x51, 0x21, 0x29, 0x2e, 0x0e, 0xe7, 0x8c, 0xd4, 0xe4, 0xec, 0xe2, 0xd2, 0x5c, 0x09, 0x46,
0x10, 0x13, 0x24, 0xe2, 0x9d, 0x5a, 0x29, 0xc1, 0xa4, 0xc0, 0xa8, 0xc1, 0x19, 0x04, 0x62, 0x0a, 0x05, 0x46, 0x0d, 0xde, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0xd9, 0x3b, 0xb5, 0x52, 0x82, 0x49,
0x89, 0x70, 0xb1, 0x7a, 0xe6, 0xa5, 0xa4, 0x56, 0x48, 0x30, 0x2b, 0x30, 0x6a, 0x30, 0x07, 0x41, 0x81, 0x51, 0x83, 0x33, 0x08, 0xc4, 0x14, 0x12, 0xe3, 0x62, 0xf3, 0x4f, 0x4b, 0x2b, 0x4e, 0x2d,
0x38, 0x20, 0xd1, 0xb0, 0xc4, 0x9c, 0xd2, 0x54, 0x09, 0x16, 0x05, 0x46, 0x0d, 0x9e, 0x20, 0x08, 0x91, 0x60, 0x56, 0x60, 0xd4, 0x60, 0x0e, 0x82, 0xf2, 0x84, 0x44, 0xb8, 0x58, 0xc3, 0x12, 0x73,
0x47, 0x48, 0x86, 0x8b, 0x33, 0x24, 0x33, 0x37, 0xb5, 0xb8, 0x24, 0x31, 0xb7, 0x40, 0x82, 0x15, 0x4a, 0x53, 0x25, 0x58, 0x14, 0x18, 0x35, 0x78, 0x82, 0x20, 0x9c, 0x24, 0x36, 0xb0, 0x5d, 0xc6,
0xac, 0x1e, 0x21, 0x90, 0xc4, 0x06, 0xb6, 0xdd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x07, 0x99, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x76, 0xd2, 0x3e, 0x83, 0x81, 0x00, 0x00, 0x00,
0x47, 0xb9, 0x93, 0x00, 0x00, 0x00,
} }

View File

@@ -3,9 +3,8 @@ syntax = "proto3";
package proto; package proto;
message Entry { message Entry {
uint32 CRC = 1; uint32 Checksum = 1;
string Key = 2; string Key = 2;
int64 Index = 3; int64 Offset = 3;
bytes Value = 4; bytes Value = 4;
int64 Timestamp = 5;
} }

View File

@@ -1,6 +1,7 @@
package streampb package streampb
import ( import (
"bufio"
"encoding/binary" "encoding/binary"
"io" "io"
@@ -16,57 +17,64 @@ const (
// NewEncoder creates a streaming protobuf encoder. // NewEncoder creates a streaming protobuf encoder.
func NewEncoder(w io.Writer) *Encoder { func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w, prefixBuf: make([]byte, prefixSize)} return &Encoder{w: bufio.NewWriter(w)}
} }
// Encoder wraps an underlying io.Writer and allows you to stream // Encoder wraps an underlying io.Writer and allows you to stream
// proto encodings on it. // proto encodings on it.
type Encoder struct { type Encoder struct {
w io.Writer w *bufio.Writer
prefixBuf []byte
} }
// Encode takes any proto.Message and streams it to the underlying writer. // Encode takes any proto.Message and streams it to the underlying writer.
// Messages are framed with a length prefix. // Messages are framed with a length prefix.
func (e *Encoder) Encode(msg proto.Message) error { func (e *Encoder) Encode(msg proto.Message) (int64, error) {
prefixBuf := make([]byte, prefixSize)
buf, err := proto.Marshal(msg) buf, err := proto.Marshal(msg)
if err != nil { if err != nil {
return err return 0, err
} }
binary.BigEndian.PutUint64(e.prefixBuf, uint64(len(buf))) binary.BigEndian.PutUint64(prefixBuf, uint64(len(buf)))
if _, err := e.w.Write(e.prefixBuf); err != nil { if _, err := e.w.Write(prefixBuf); err != nil {
return errors.Wrap(err, "failed writing length prefix") return 0, errors.Wrap(err, "failed writing length prefix")
} }
_, err = e.w.Write(buf) n, err := e.w.Write(buf)
return errors.Wrap(err, "failed writing marshaled data") if err != nil {
return 0, errors.Wrap(err, "failed writing marshaled data")
}
if err = e.w.Flush(); err != nil {
return 0, errors.Wrap(err, "failed flushing data")
}
return int64(n + prefixSize), nil
} }
// NewDecoder creates a streaming protobuf decoder. // NewDecoder creates a streaming protobuf decoder.
func NewDecoder(r io.Reader) *Decoder { func NewDecoder(r io.Reader) *Decoder {
return &Decoder{ return &Decoder{r: r}
r: r,
prefixBuf: make([]byte, prefixSize),
}
} }
// Decoder wraps an underlying io.Reader and allows you to stream // Decoder wraps an underlying io.Reader and allows you to stream
// proto decodings on it. // proto decodings on it.
type Decoder struct { type Decoder struct {
r io.Reader r io.Reader
prefixBuf []byte
} }
// Decode takes a proto.Message and unmarshals the next payload in the // Decode takes a proto.Message and unmarshals the next payload in the
// underlying io.Reader. It returns an EOF when it's done. // underlying io.Reader. It returns an EOF when it's done.
func (d *Decoder) Decode(v proto.Message) error { func (d *Decoder) Decode(v proto.Message) error {
_, err := io.ReadFull(d.r, d.prefixBuf) prefixBuf := make([]byte, prefixSize)
_, err := io.ReadFull(d.r, prefixBuf)
if err != nil { if err != nil {
return err return err
} }
n := binary.BigEndian.Uint64(d.prefixBuf) n := binary.BigEndian.Uint64(prefixBuf)
buf := make([]byte, n) buf := make([]byte, n)

36
internal/utils.go Normal file
View File

@@ -0,0 +1,36 @@
package internal
import (
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
)
func GetDatafiles(path string) ([]string, error) {
fns, err := filepath.Glob(fmt.Sprintf("%s/*.data", path))
if err != nil {
return nil, err
}
sort.Strings(fns)
return fns, nil
}
func ParseIds(fns []string) ([]int, error) {
var ids []int
for _, fn := range fns {
fn = filepath.Base(fn)
ext := filepath.Ext(fn)
if ext != ".data" {
continue
}
id, err := strconv.ParseInt(strings.TrimSuffix(fn, ext), 10, 32)
if err != nil {
return nil, err
}
ids = append(ids, int(id))
}
sort.Ints(ids)
return ids, nil
}

View File

@@ -1,4 +1,4 @@
package bitcask package internal
import ( import (
"fmt" "fmt"

View File

@@ -1,4 +1,4 @@
package bitcask package internal
import ( import (
"fmt" "fmt"

47
options.go Normal file
View File

@@ -0,0 +1,47 @@
package bitcask
const (
DefaultMaxDatafileSize = 1 << 20 // 1MB
DefaultMaxKeySize = 64 // 64 bytes
DefaultMaxValueSize = 1 << 16 // 65KB
)
// Option ...
type Option option
type option func(*config) error
type config struct {
MaxDatafileSize int
MaxKeySize int
MaxValueSize int
}
func NewDefaultConfig() *config {
return &config{
MaxDatafileSize: DefaultMaxDatafileSize,
MaxKeySize: DefaultMaxKeySize,
MaxValueSize: DefaultMaxValueSize,
}
}
func WithMaxDatafileSize(size int) option {
return func(cfg *config) error {
cfg.MaxDatafileSize = size
return nil
}
}
func WithMaxKeySize(size int) option {
return func(cfg *config) error {
cfg.MaxKeySize = size
return nil
}
}
func WithMaxValueSize(size int) option {
return func(cfg *config) error {
cfg.MaxValueSize = size
return nil
}
}