mirror of
https://github.com/jimeh/ozu.io.git
synced 2026-02-19 08:06:39 +00:00
Noramlize Store interface
Instead of exposing a key/value get/set style interface, design the interface around the actions that are actually required. This is a first step towards supporting adding a Store to support Google Cloud's Datastore.
This commit is contained in:
@@ -1,17 +1,19 @@
|
|||||||
package goleveldbstore
|
package goleveldbstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/jimeh/ozu.io/storage"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const errLeveldbNotFound = "leveldb: not found"
|
||||||
|
|
||||||
// DefaultSequenceKey is used by NextSequence().
|
// DefaultSequenceKey is used by NextSequence().
|
||||||
var DefaultSequenceKey = []byte("__SEQUENCE_ID__")
|
var DefaultSequenceKey = []byte("__SEQUENCE_ID__")
|
||||||
|
|
||||||
// ErrNotFound is returned when Get() tries to fetch a non-existent key.
|
var uidKeyPrefix = []byte("!")
|
||||||
var ErrNotFound = errors.New("not found")
|
var urlKeyPrefix = []byte("#")
|
||||||
|
|
||||||
// New creates a new Store using given path to persist data.
|
// New creates a new Store using given path to persist data.
|
||||||
func New(path string) (*Store, error) {
|
func New(path string) (*Store, error) {
|
||||||
@@ -39,33 +41,117 @@ func (s *Store) Close() error {
|
|||||||
return s.DB.Close()
|
return s.DB.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a given key's value.
|
// Create a given Record.
|
||||||
func (s *Store) Get(key []byte) ([]byte, error) {
|
func (s *Store) Create(uid []byte, url []byte) (*storage.Record, error) {
|
||||||
value, err := s.DB.Get(key, nil)
|
tx, err := s.DB.OpenTransaction()
|
||||||
if err != nil && err.Error() == "leveldb: not found" {
|
if err != nil {
|
||||||
return nil, ErrNotFound
|
return &storage.Record{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return value, err
|
err = tx.Put(s.uidKey(uid), url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Put(s.urlKey(url), uid, nil)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &storage.Record{UID: uid, URL: url}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a given key's to the specified value.
|
// FindByUID looks up records based on their UID.
|
||||||
func (s *Store) Set(key []byte, value []byte) error {
|
func (s *Store) FindByUID(uid []byte) (*storage.Record, error) {
|
||||||
return s.DB.Put(key, value, nil)
|
value, err := s.DB.Get(s.uidKey(uid), nil)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == errLeveldbNotFound {
|
||||||
|
return &storage.Record{}, storage.ErrNotFound
|
||||||
|
}
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &storage.Record{UID: uid, URL: value}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a given key.
|
// FindByURL looks up records based on their URL.
|
||||||
func (s *Store) Delete(key []byte) error {
|
func (s *Store) FindByURL(url []byte) (*storage.Record, error) {
|
||||||
return s.DB.Delete(key, nil)
|
value, err := s.DB.Get(s.urlKey(url), nil)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == errLeveldbNotFound {
|
||||||
|
return &storage.Record{}, storage.ErrNotFound
|
||||||
|
}
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &storage.Record{UID: value, URL: url}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByUID deletes records based on their UID.
|
||||||
|
func (s *Store) DeleteByUID(uid []byte) (*storage.Record, error) {
|
||||||
|
record, err := s.FindByUID(uid)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.delete(record)
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByURL deletes records based on their URL.
|
||||||
|
func (s *Store) DeleteByURL(url []byte) (*storage.Record, error) {
|
||||||
|
record, err := s.FindByURL(url)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.delete(record)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) delete(r *storage.Record) error {
|
||||||
|
tx, err := s.DB.OpenTransaction()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Delete(s.uidKey(r.UID), nil)
|
||||||
|
if err != nil && err.Error() == errLeveldbNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Delete(s.urlKey(r.URL), nil)
|
||||||
|
if err != nil && err.Error() == errLeveldbNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) uidKey(uid []byte) []byte {
|
||||||
|
return append(uidKeyPrefix, uid...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) urlKey(url []byte) []byte {
|
||||||
|
return append(urlKeyPrefix, url...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextSequence returns a auto-incrementing int.
|
// NextSequence returns a auto-incrementing int.
|
||||||
func (s *Store) NextSequence() (int, error) {
|
func (s *Store) NextSequence() (int, error) {
|
||||||
return s.Incr(s.SequenceKey)
|
return s.incr(s.SequenceKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incr increments a given key (must be numeric-like value)
|
// Incr increments a given key (must be numeric-like value)
|
||||||
func (s *Store) Incr(key []byte) (int, error) {
|
func (s *Store) incr(key []byte) (int, error) {
|
||||||
tx, err := s.DB.OpenTransaction()
|
tx, err := s.DB.OpenTransaction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, err
|
return -1, err
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ import (
|
|||||||
|
|
||||||
var testDbPath = "./goleveldb_test_data"
|
var testDbPath = "./goleveldb_test_data"
|
||||||
|
|
||||||
var examples = []struct {
|
var examples = []storage.Record{
|
||||||
key []byte
|
storage.Record{UID: []byte("Kb8X"), URL: []byte("https://google.com/")},
|
||||||
value []byte
|
storage.Record{UID: []byte("h3mz"), URL: []byte("https://github.com/")},
|
||||||
}{
|
storage.Record{UID: []byte("3qxs"), URL: []byte("https://twitter.com/")},
|
||||||
{key: []byte("hello"), value: []byte("world")},
|
|
||||||
{key: []byte("foo"), value: []byte("bar")},
|
|
||||||
{key: []byte("wtf"), value: []byte("dude")},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreSuite struct {
|
type StoreSuite struct {
|
||||||
@@ -42,7 +39,9 @@ func (s *StoreSuite) TearDownTest() {
|
|||||||
|
|
||||||
func (s *StoreSuite) Seed() {
|
func (s *StoreSuite) Seed() {
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
err := s.db.Put(e.key, e.value, nil)
|
err := s.db.Put(append(uidKeyPrefix, e.UID...), e.URL, nil)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
err = s.db.Put(append(urlKeyPrefix, e.URL...), e.UID, nil)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,62 +52,112 @@ func (s *StoreSuite) TestStoreInterface() {
|
|||||||
s.Implements(new(storage.Store), new(Store))
|
s.Implements(new(storage.Store), new(Store))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestSet() {
|
func (s *StoreSuite) TestCreate() {
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
err := s.store.Set(e.key, e.value)
|
record, err := s.store.Create(e.UID, e.URL)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
s.Equal(e.UID, record.UID)
|
||||||
|
s.Equal(e.URL, record.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
result, _ := s.db.Get(e.key, nil)
|
recordURL, _ := s.db.Get(append(uidKeyPrefix, e.UID...), nil)
|
||||||
s.Equal(e.value, result)
|
s.Equal(e.URL, recordURL)
|
||||||
|
recordUID, _ := s.db.Get(append(urlKeyPrefix, e.URL...), nil)
|
||||||
|
s.Equal(e.UID, recordUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestGetExisting() {
|
func (s *StoreSuite) TestFindExistingByUID() {
|
||||||
s.Seed()
|
s.Seed()
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
result, err := s.store.Get(e.key)
|
record, err := s.store.FindByUID(e.UID)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(e.value, result)
|
s.Equal(e.UID, record.UID)
|
||||||
|
s.Equal(e.URL, record.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestGetNonExistant() {
|
func (s *StoreSuite) TestFindNonExistantByUID() {
|
||||||
result, err := s.store.Get([]byte("does-not-exist"))
|
record, err := s.store.FindByUID([]byte("nope"))
|
||||||
s.Nil(result)
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
s.EqualError(err, "not found")
|
s.EqualError(err, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestDeleteExisting() {
|
func (s *StoreSuite) TestFindExistingByURL() {
|
||||||
s.Seed()
|
s.Seed()
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
value, _ := s.db.Get(e.key, nil)
|
record, err := s.store.FindByURL(e.URL)
|
||||||
s.Require().Equal(e.value, value)
|
|
||||||
|
|
||||||
value, err := s.store.Get(e.key)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.Require().Equal(value, e.value)
|
|
||||||
|
|
||||||
err = s.store.Delete(e.key)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
s.Equal(e.UID, record.UID)
|
||||||
value, err = s.store.Get(e.key)
|
s.Equal(e.URL, record.URL)
|
||||||
s.Nil(value)
|
|
||||||
s.EqualError(err, "not found")
|
|
||||||
|
|
||||||
has, _ := s.db.Has(e.key, nil)
|
|
||||||
s.Equal(false, has)
|
|
||||||
result, _ := s.db.Get(e.key, nil)
|
|
||||||
s.Equal([]byte{}, result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestDeleteNonExistant() {
|
func (s *StoreSuite) TestFindNonExistantByURL() {
|
||||||
err := s.store.Delete([]byte("does-not-exist"))
|
record, err := s.store.FindByURL([]byte("http://nope.com/"))
|
||||||
s.NoError(err)
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteExistingByUID() {
|
||||||
|
s.Seed()
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
record, err := s.store.DeleteByUID(e.UID)
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(record.UID, e.UID)
|
||||||
|
s.Equal(record.URL, e.URL)
|
||||||
|
|
||||||
|
record, err = s.store.FindByUID(e.UID)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
|
||||||
|
record, err = s.store.FindByURL(e.URL)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteNonExistantByUID() {
|
||||||
|
record, err := s.store.DeleteByUID([]byte("nope"))
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteExistingByURL() {
|
||||||
|
s.Seed()
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
record, err := s.store.DeleteByURL(e.URL)
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(record.UID, e.UID)
|
||||||
|
s.Equal(record.URL, e.URL)
|
||||||
|
|
||||||
|
record, err = s.store.FindByUID(e.UID)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
|
||||||
|
record, err = s.store.FindByURL(e.URL)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteNonExistantByURL() {
|
||||||
|
record, err := s.store.DeleteByURL([]byte("http://nope/"))
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestNextSequenceExisting() {
|
func (s *StoreSuite) TestNextSequenceExisting() {
|
||||||
@@ -135,14 +184,14 @@ func (s *StoreSuite) TestIncrExisting() {
|
|||||||
err := s.db.Put(key, []byte("5"), nil)
|
err := s.db.Put(key, []byte("5"), nil)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
result, err := s.store.Incr(key)
|
result, err := s.store.incr(key)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(6, result)
|
s.Equal(6, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestIncrNonExistant() {
|
func (s *StoreSuite) TestIncrNonExistant() {
|
||||||
for i := 1; i < 10; i++ {
|
for i := 1; i < 10; i++ {
|
||||||
result, err := s.store.Incr([]byte("counter"))
|
result, err := s.store.incr([]byte("counter"))
|
||||||
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(i, result)
|
s.Equal(i, result)
|
||||||
@@ -157,54 +206,57 @@ func TestStoreSuite(t *testing.T) {
|
|||||||
|
|
||||||
// Benchmarks
|
// Benchmarks
|
||||||
|
|
||||||
func BenchmarkGet(b *testing.B) {
|
func BenchmarkCreate(b *testing.B) {
|
||||||
store, _ := New(testDbPath)
|
store, _ := New(testDbPath)
|
||||||
|
|
||||||
key := []byte("hello")
|
uid := []byte("Kb8X")
|
||||||
value := []byte("world")
|
url := []byte("https://google.com/")
|
||||||
_ = store.Set(key, value)
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_, _ = store.Get(key)
|
store.Create(append(uid, string(n)...), url)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
_ = os.RemoveAll(testDbPath)
|
os.RemoveAll(testDbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkSet(b *testing.B) {
|
func BenchmarkFindByUID(b *testing.B) {
|
||||||
store, _ := New(testDbPath)
|
store, _ := New(testDbPath)
|
||||||
|
|
||||||
key := []byte("hello")
|
uid := []byte("Kb8X")
|
||||||
value := []byte("world")
|
url := []byte("https://google.com/")
|
||||||
|
store.Create(uid, url)
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_ = store.Set(append(key, string(n)...), value)
|
store.FindByUID(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
_ = os.RemoveAll(testDbPath)
|
os.RemoveAll(testDbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFindByURL(b *testing.B) {
|
||||||
|
store, _ := New(testDbPath)
|
||||||
|
|
||||||
|
uid := []byte("Kb8X")
|
||||||
|
url := []byte("https://google.com/")
|
||||||
|
store.Create(uid, url)
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
store.FindByURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Close()
|
||||||
|
os.RemoveAll(testDbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNextSequence(b *testing.B) {
|
func BenchmarkNextSequence(b *testing.B) {
|
||||||
store, _ := New(testDbPath)
|
store, _ := New(testDbPath)
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_, _ = store.NextSequence()
|
store.NextSequence()
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
_ = os.RemoveAll(testDbPath)
|
os.RemoveAll(testDbPath)
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkIncr(b *testing.B) {
|
|
||||||
store, _ := New(testDbPath)
|
|
||||||
|
|
||||||
key := []byte("incr-benchmark-counter")
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
_, _ = store.Incr(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = store.Close()
|
|
||||||
_ = os.RemoveAll(testDbPath)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
package inmemorystore
|
package inmemorystore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNotFound is returned when Get() tries to fetch a non-existent key.
|
"github.com/jimeh/ozu.io/storage"
|
||||||
var ErrNotFound = errors.New("not found")
|
)
|
||||||
|
|
||||||
// New creates a new Store using given path to persist data.
|
// New creates a new Store using given path to persist data.
|
||||||
func New() (*Store, error) {
|
func New() (*Store, error) {
|
||||||
store := &Store{
|
store := &Store{
|
||||||
Data: map[string][]byte{},
|
UIDMap: map[string][]byte{},
|
||||||
|
URLMap: map[string][]byte{},
|
||||||
Sequence: 0,
|
Sequence: 0,
|
||||||
}
|
}
|
||||||
return store, nil
|
return store, nil
|
||||||
@@ -20,43 +19,79 @@ func New() (*Store, error) {
|
|||||||
// Store allows storing data into a in-memory map.
|
// Store allows storing data into a in-memory map.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
Data map[string][]byte
|
UIDMap map[string][]byte
|
||||||
|
URLMap map[string][]byte
|
||||||
Sequence int
|
Sequence int
|
||||||
Closed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close database.
|
// Close database.
|
||||||
func (s *Store) Close() error {
|
func (s *Store) Close() error {
|
||||||
s.Data = make(map[string][]byte)
|
s.Lock()
|
||||||
|
s.UIDMap = make(map[string][]byte)
|
||||||
|
s.URLMap = make(map[string][]byte)
|
||||||
s.Sequence = 0
|
s.Sequence = 0
|
||||||
|
s.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a given key's value.
|
// Create a given Record
|
||||||
func (s *Store) Get(key []byte) ([]byte, error) {
|
func (s *Store) Create(uid []byte, url []byte) (*storage.Record, error) {
|
||||||
|
s.Lock()
|
||||||
|
s.UIDMap[string(uid)] = url
|
||||||
|
s.URLMap[string(url)] = uid
|
||||||
|
s.Unlock()
|
||||||
|
return &storage.Record{UID: uid, URL: url}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByUID looks up records based on their UID.
|
||||||
|
func (s *Store) FindByUID(uid []byte) (*storage.Record, error) {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
value := s.Data[string(key)]
|
value := s.UIDMap[string(uid)]
|
||||||
s.RUnlock()
|
s.RUnlock()
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil, ErrNotFound
|
return &storage.Record{}, storage.ErrNotFound
|
||||||
}
|
}
|
||||||
return value, nil
|
return &storage.Record{UID: uid, URL: value}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a given key's to the specified value.
|
// FindByURL looks up records based on their URL.
|
||||||
func (s *Store) Set(key []byte, value []byte) error {
|
func (s *Store) FindByURL(url []byte) (*storage.Record, error) {
|
||||||
s.Lock()
|
s.RLock()
|
||||||
s.Data[string(key)] = value
|
value := s.URLMap[string(url)]
|
||||||
s.Unlock()
|
s.RUnlock()
|
||||||
return nil
|
if value == nil {
|
||||||
|
return &storage.Record{}, storage.ErrNotFound
|
||||||
|
}
|
||||||
|
return &storage.Record{UID: value, URL: url}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a given key.
|
// DeleteByUID deletes records based on their UID.
|
||||||
func (s *Store) Delete(key []byte) error {
|
func (s *Store) DeleteByUID(uid []byte) (*storage.Record, error) {
|
||||||
|
record, err := s.FindByUID(uid)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.delete(record)
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByURL deletes records based on their URL.
|
||||||
|
func (s *Store) DeleteByURL(url []byte) (*storage.Record, error) {
|
||||||
|
record, err := s.FindByURL(url)
|
||||||
|
if err != nil {
|
||||||
|
return &storage.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.delete(record)
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) delete(r *storage.Record) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
delete(s.Data, string(key))
|
delete(s.UIDMap, string(r.UID))
|
||||||
|
delete(s.URLMap, string(r.URL))
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextSequence returns a auto-incrementing int.
|
// NextSequence returns a auto-incrementing int.
|
||||||
|
|||||||
@@ -9,13 +9,10 @@ import (
|
|||||||
|
|
||||||
// Setup Suite
|
// Setup Suite
|
||||||
|
|
||||||
var examples = []struct {
|
var examples = []storage.Record{
|
||||||
key []byte
|
storage.Record{UID: []byte("Kb8X"), URL: []byte("https://google.com/")},
|
||||||
value []byte
|
storage.Record{UID: []byte("h3mz"), URL: []byte("https://github.com/")},
|
||||||
}{
|
storage.Record{UID: []byte("3qxs"), URL: []byte("https://twitter.com/")},
|
||||||
{key: []byte("hello"), value: []byte("world")},
|
|
||||||
{key: []byte("foo"), value: []byte("bar")},
|
|
||||||
{key: []byte("wtf"), value: []byte("dude")},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreSuite struct {
|
type StoreSuite struct {
|
||||||
@@ -35,7 +32,8 @@ func (s *StoreSuite) TearDownTest() {
|
|||||||
|
|
||||||
func (s *StoreSuite) Seed() {
|
func (s *StoreSuite) Seed() {
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
s.store.Data[string(e.key)] = e.value
|
s.store.UIDMap[string(e.UID)] = e.URL
|
||||||
|
s.store.URLMap[string(e.URL)] = e.UID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,60 +43,112 @@ func (s *StoreSuite) TestStoreInterface() {
|
|||||||
s.Implements(new(storage.Store), new(Store))
|
s.Implements(new(storage.Store), new(Store))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestSet() {
|
func (s *StoreSuite) TestCreate() {
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
err := s.store.Set(e.key, e.value)
|
record, err := s.store.Create(e.UID, e.URL)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
s.Equal(e.UID, record.UID)
|
||||||
|
s.Equal(e.URL, record.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
result, _ := s.store.Data[string(e.key)]
|
recordURL, _ := s.store.UIDMap[string(e.UID)]
|
||||||
s.Equal(e.value, result)
|
s.Equal(e.URL, recordURL)
|
||||||
|
recordUID, _ := s.store.URLMap[string(e.URL)]
|
||||||
|
s.Equal(e.UID, recordUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestGetExisting() {
|
func (s *StoreSuite) TestFindExistingByUID() {
|
||||||
s.Seed()
|
s.Seed()
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
result, err := s.store.Get(e.key)
|
record, err := s.store.FindByUID(e.UID)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(e.value, result)
|
s.Equal(e.UID, record.UID)
|
||||||
|
s.Equal(e.URL, record.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestGetNonExistant() {
|
func (s *StoreSuite) TestFindNonExistantByUID() {
|
||||||
result, err := s.store.Get([]byte("does-not-exist"))
|
record, err := s.store.FindByUID([]byte("does-not-exist"))
|
||||||
s.Nil(result)
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
s.EqualError(err, "not found")
|
s.EqualError(err, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestDeleteExisting() {
|
func (s *StoreSuite) TestFindExistingByURL() {
|
||||||
s.Seed()
|
s.Seed()
|
||||||
|
|
||||||
for _, e := range examples {
|
for _, e := range examples {
|
||||||
value := s.store.Data[string(e.key)]
|
record, err := s.store.FindByURL(e.URL)
|
||||||
s.Require().Equal(e.value, value)
|
|
||||||
|
|
||||||
value, err := s.store.Get(e.key)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.Require().Equal(value, e.value)
|
|
||||||
|
|
||||||
err = s.store.Delete(e.key)
|
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
|
s.Equal(e.UID, record.UID)
|
||||||
value, err = s.store.Get(e.key)
|
s.Equal(e.URL, record.URL)
|
||||||
s.Nil(value)
|
|
||||||
s.EqualError(err, "not found")
|
|
||||||
|
|
||||||
_, has := s.store.Data[string(e.key)]
|
|
||||||
s.Equal(false, has)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestDeleteNonExistant() {
|
func (s *StoreSuite) TestFindNonExistantByURL() {
|
||||||
err := s.store.Delete([]byte("does-not-exist"))
|
record, err := s.store.FindByURL([]byte("http://nope.com/"))
|
||||||
s.NoError(err)
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteExistingByUID() {
|
||||||
|
s.Seed()
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
record, err := s.store.DeleteByUID(e.UID)
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(record.UID, e.UID)
|
||||||
|
s.Equal(record.URL, e.URL)
|
||||||
|
|
||||||
|
record, err = s.store.FindByUID(e.UID)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
|
||||||
|
record, err = s.store.FindByURL(e.URL)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteNonExistantByUID() {
|
||||||
|
record, err := s.store.DeleteByUID([]byte("nope"))
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteExistingByURL() {
|
||||||
|
s.Seed()
|
||||||
|
|
||||||
|
for _, e := range examples {
|
||||||
|
record, err := s.store.DeleteByURL(e.URL)
|
||||||
|
s.NoError(err)
|
||||||
|
s.Equal(record.UID, e.UID)
|
||||||
|
s.Equal(record.URL, e.URL)
|
||||||
|
|
||||||
|
record, err = s.store.FindByUID(e.UID)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
|
||||||
|
record, err = s.store.FindByURL(e.URL)
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StoreSuite) TestDeleteNonExistantByURL() {
|
||||||
|
record, err := s.store.DeleteByURL([]byte("http://nope/"))
|
||||||
|
s.Nil(record.UID)
|
||||||
|
s.Nil(record.URL)
|
||||||
|
s.EqualError(err, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StoreSuite) TestNextSequenceExisting() {
|
func (s *StoreSuite) TestNextSequenceExisting() {
|
||||||
@@ -126,39 +176,53 @@ func TestStoreSuite(t *testing.T) {
|
|||||||
|
|
||||||
// Benchmarks
|
// Benchmarks
|
||||||
|
|
||||||
func BenchmarkGet(b *testing.B) {
|
func BenchmarkCreate(b *testing.B) {
|
||||||
store, _ := New()
|
store, _ := New()
|
||||||
|
|
||||||
key := []byte("hello")
|
uid := []byte("Kb8X")
|
||||||
value := []byte("world")
|
url := []byte("https://google.com/")
|
||||||
_ = store.Set(key, value)
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_, _ = store.Get(key)
|
store.Create(append(uid, string(n)...), url)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkSet(b *testing.B) {
|
func BenchmarkFindByUID(b *testing.B) {
|
||||||
store, _ := New()
|
store, _ := New()
|
||||||
|
|
||||||
key := []byte("hello")
|
uid := []byte("Kb8X")
|
||||||
value := []byte("world")
|
url := []byte("https://google.com/")
|
||||||
|
store.Create(uid, url)
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_ = store.Set(append(key, string(n)...), value)
|
store.FindByUID(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFindByURL(b *testing.B) {
|
||||||
|
store, _ := New()
|
||||||
|
|
||||||
|
uid := []byte("Kb8X")
|
||||||
|
url := []byte("https://google.com/")
|
||||||
|
store.Create(uid, url)
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
store.FindByURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNextSequence(b *testing.B) {
|
func BenchmarkNextSequence(b *testing.B) {
|
||||||
store, _ := New()
|
store, _ := New()
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
_, _ = store.NextSequence()
|
store.NextSequence()
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = store.Close()
|
store.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
// Store defines a standard interface for storage.
|
import "errors"
|
||||||
|
|
||||||
|
// ErrNotFound is the default error message when data is not found.
|
||||||
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
|
// Store defines a standard interface for storage
|
||||||
type Store interface {
|
type Store interface {
|
||||||
Close() error
|
Close() error
|
||||||
Get([]byte) ([]byte, error)
|
Create(UID []byte, URL []byte) (*Record, error)
|
||||||
Set([]byte, []byte) error
|
FindByUID(UID []byte) (*Record, error)
|
||||||
Delete([]byte) error
|
FindByURL(URL []byte) (*Record, error)
|
||||||
|
DeleteByUID(UID []byte) (*Record, error)
|
||||||
|
DeleteByURL(URL []byte) (*Record, error)
|
||||||
NextSequence() (int, error)
|
NextSequence() (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record provides a standard way to refer to a shortened URL.
|
||||||
|
type Record struct {
|
||||||
|
UID []byte
|
||||||
|
URL []byte
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user