From 53fa0bd3973fa389991d03aa7766849dfac43748 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sun, 3 Jul 2016 02:09:46 +0100 Subject: [PATCH] Initial work on storage package --- storage/goleveldb_store.go | 81 ++++++++++++++++ storage/goleveldb_store_test.go | 167 ++++++++++++++++++++++++++++++++ storage/store.go | 10 ++ 3 files changed, 258 insertions(+) create mode 100644 storage/goleveldb_store.go create mode 100644 storage/goleveldb_store_test.go create mode 100644 storage/store.go diff --git a/storage/goleveldb_store.go b/storage/goleveldb_store.go new file mode 100644 index 0000000..4176488 --- /dev/null +++ b/storage/goleveldb_store.go @@ -0,0 +1,81 @@ +package storage + +import ( + "strconv" + + "github.com/syndtr/goleveldb/leveldb" +) + +// GoleveldbStore allows storing data into a goleveldb database. +type GoleveldbStore struct { + DB *leveldb.DB +} + +// NewGoleveldbStore creates a new GoleveldbStore using given path to persist +// data. +func NewGoleveldbStore(path string) (GoleveldbStore, error) { + db, err := leveldb.OpenFile(path, nil) + if err != nil { + return GoleveldbStore{}, err + } + + return GoleveldbStore{DB: db}, nil +} + +// Close underlying goleveldb database. +func (s *GoleveldbStore) Close() error { + return s.DB.Close() +} + +// Get a given key's value. +func (s *GoleveldbStore) Get(key []byte) ([]byte, error) { + value, err := s.DB.Get(key, nil) + if err != nil && err.Error() == "leveldb: not found" { + return value, nil + } + + return value, err +} + +// Set a given key's to the specified value. +func (s *GoleveldbStore) Set(key []byte, value []byte) error { + return s.DB.Put(key, value, nil) +} + +// Delete a given key. +func (s *GoleveldbStore) Delete(key []byte) error { + return s.DB.Delete(key, nil) +} + +// Incr increments a given key (must be numeric-like value) +func (s *GoleveldbStore) Incr(key []byte) (int, error) { + tx, err := s.DB.OpenTransaction() + if err != nil { + return -1, err + } + + value, err := tx.Get(key, nil) + if value == nil { + value = []byte("0") + } + + num, err := strconv.Atoi(string(value)) + if err != nil { + return -1, err + } + + num++ + value = []byte(strconv.Itoa(num)) + + err = tx.Put(key, value, nil) + if err != nil { + return -1, err + } + + err = tx.Commit() + if err != nil { + return -1, err + } + + return num, nil +} diff --git a/storage/goleveldb_store_test.go b/storage/goleveldb_store_test.go new file mode 100644 index 0000000..ad50891 --- /dev/null +++ b/storage/goleveldb_store_test.go @@ -0,0 +1,167 @@ +package storage + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/syndtr/goleveldb/leveldb" +) + +// Setup Suite + +var testDbPath = "./goleveldb_test_data" + +var examples = []struct { + key []byte + value []byte +}{ + {key: []byte("hello"), value: []byte("world")}, + {key: []byte("foo"), value: []byte("bar")}, + {key: []byte("wtf"), value: []byte("dude")}, +} + +type GoleveldbStoreSuite struct { + suite.Suite + store Store + db *leveldb.DB +} + +func (s *GoleveldbStoreSuite) Seed() { + for _, e := range examples { + err := s.db.Put(e.key, e.value, nil) + s.Require().Nil(err) + } +} + +func (s *GoleveldbStoreSuite) SetupTest() { + store, err := NewGoleveldbStore(testDbPath) + s.Nil(err) + s.store = &store + s.db = store.DB +} + +func (s *GoleveldbStoreSuite) TearDownTest() { + s.store.Close() + os.RemoveAll(testDbPath) +} + +// Tests + +func (s *GoleveldbStoreSuite) TestSet() { + for _, e := range examples { + err := s.store.Set(e.key, e.value) + s.Nil(err) + } + + for _, e := range examples { + result, _ := s.db.Get(e.key, nil) + s.Equal(e.value, result) + } +} + +func (s *GoleveldbStoreSuite) TestGetExisting() { + s.Seed() + + for _, e := range examples { + result, err := s.store.Get(e.key) + s.Nil(err) + s.Equal(e.value, result) + } +} + +func (s *GoleveldbStoreSuite) TestGetNonExistant() { + result, err := s.store.Get([]byte("does-not-exist")) + s.Nil(err) + s.Nil(result) +} + +func (s *GoleveldbStoreSuite) TestDeleteExisting() { + s.Seed() + + for _, e := range examples { + value, _ := s.db.Get(e.key, nil) + s.Require().Equal(e.value, value) + + err := s.store.Delete(e.key) + s.Nil(err) + + has, _ := s.db.Has(e.key, nil) + s.Equal(false, has) + result, _ := s.db.Get(e.key, nil) + s.Equal([]byte{}, result) + } +} + +func (s *GoleveldbStoreSuite) TestDeleteNonExistant() { + err := s.store.Delete([]byte("does-not-exist")) + s.Nil(err) +} + +func (s *GoleveldbStoreSuite) TestIncrExisting() { + key := []byte("my-counter") + + err := s.db.Put(key, []byte("5"), nil) + s.Require().Nil(err) + + result, err := s.store.Incr(key) + s.Nil(err) + s.Equal(6, result) +} + +func (s *GoleveldbStoreSuite) TestIncrNonExistant() { + for i := 1; i < 10; i++ { + result, err := s.store.Incr([]byte("counter")) + + s.Nil(err) + s.Equal(i, result) + } +} + +// Run Suite + +func TestGoleveldbStoreSuite(t *testing.T) { + suite.Run(t, new(GoleveldbStoreSuite)) +} + +// Benchmarks + +func BenchmarkGet(b *testing.B) { + key := []byte("hello") + value := []byte("world") + store, _ := NewGoleveldbStore(testDbPath) + _ = store.Set(key, value) + + for n := 0; n < b.N; n++ { + _, _ = store.Get(key) + } + + _ = store.Close() + _ = os.RemoveAll(testDbPath) +} + +func BenchmarkSet(b *testing.B) { + key := []byte("hello") + value := []byte("world") + store, _ := NewGoleveldbStore(testDbPath) + _ = store.Set(key, value) + + for n := 0; n < b.N; n++ { + _ = store.Set(append(key, string(n)...), append(value, string(n)...)) + } + + _ = store.Close() + _ = os.RemoveAll(testDbPath) +} + +func BenchmarkIncr(b *testing.B) { + key := []byte("incr-benchmark-counter") + store, _ := NewGoleveldbStore(testDbPath) + + for n := 0; n < b.N; n++ { + _, _ = store.Incr(key) + } + + _ = store.Close() + _ = os.RemoveAll(testDbPath) +} diff --git a/storage/store.go b/storage/store.go new file mode 100644 index 0000000..7cf1270 --- /dev/null +++ b/storage/store.go @@ -0,0 +1,10 @@ +package storage + +// Storage defines a standard interface for storage. +type Store interface { + Close() error + Get([]byte) ([]byte, error) + Set([]byte, []byte) error + Delete([]byte) error + Incr([]byte) (int, error) +}