Skip to content

Commit

Permalink
Merge pull request #20 from chenyanchen/refactor/layered-kv
Browse files Browse the repository at this point in the history
feat: implement layerKV and batchKV structures with caching functiona…
  • Loading branch information
chenyanchen authored Dec 10, 2024
2 parents 1605f51 + 2b3c4ee commit 24c0996
Show file tree
Hide file tree
Showing 7 changed files with 586 additions and 129 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.23
require (
github.com/chenyanchen/sync v0.5.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.10.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
73 changes: 73 additions & 0 deletions layerkv/batch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package layerkv

import (
"context"
"errors"
"maps"
"slices"

"github.com/chenyanchen/db"
)

type batch[K comparable, V any] struct {
cache db.BatchKV[K, V]
store db.BatchKV[K, V]
}

func NewBatch[K comparable, V any](cache, store db.BatchKV[K, V]) (*batch[K, V], error) {
if cache == nil {
return nil, errors.New("cache is nil")
}
if store == nil {
return nil, errors.New("store is nil")
}
return &batch[K, V]{
cache: cache,
store: store,
}, nil
}

func (l batch[K, V]) Get(ctx context.Context, keys []K) (map[K]V, error) {
cache, err := l.cache.Get(ctx, keys)
if err != nil {
return nil, err
}

if len(cache) == len(keys) {
return cache, nil
}

miss := make([]K, 0, len(keys)-len(cache))
for _, key := range keys {
if _, ok := cache[key]; !ok {
miss = append(miss, key)
}
}

store, err := l.store.Get(ctx, miss)
if err != nil {
return nil, err
}

for k, v := range store {
cache[k] = v
}

return cache, l.cache.Set(ctx, store)
}

func (l batch[K, V]) Set(ctx context.Context, kvs map[K]V) error {
if err := l.store.Set(ctx, kvs); err != nil {
return err
}

return l.cache.Del(ctx, slices.Collect(maps.Keys(kvs)))
}

func (l batch[K, V]) Del(ctx context.Context, keys []K) error {
if err := l.store.Del(ctx, keys); err != nil {
return err
}

return l.cache.Del(ctx, keys)
}
205 changes: 205 additions & 0 deletions layerkv/batch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package layerkv

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"

"github.com/chenyanchen/db/mocks"
)

func Test_batch_Get(t *testing.T) {
type args[K comparable] struct {
ctx context.Context
keys []K
}
type testCase[K comparable, V any] struct {
name string
l batch[K, V]
args args[K]
want map[K]V
wantErr assert.ErrorAssertionFunc
}
tests := []testCase[string, string]{
{
name: "cache error",
l: batch[string, string]{
cache: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return nil, assert.AnError
},
},
},
args: args[string]{context.Background(), []string{"key1", "key2"}},
want: nil,
wantErr: assert.Error,
}, {
name: "all from cache",
l: batch[string, string]{
cache: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return map[string]string{
"key1": "value1",
"key2": "value2",
}, nil
},
},
},
args: args[string]{context.Background(), []string{"key1", "key2"}},
want: map[string]string{
"key1": "value1",
"key2": "value2",
},
wantErr: assert.NoError,
}, {
name: "store error",
l: batch[string, string]{
cache: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return map[string]string{
"key1": "value1",
}, nil
},
},
store: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return nil, assert.AnError
},
},
},
args: args[string]{context.Background(), []string{"key1", "key2"}},
want: nil,
wantErr: assert.Error,
}, {
name: "mixed",
l: batch[string, string]{
cache: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return map[string]string{"key1": "value1"}, nil
},
SetFunc: func(ctx context.Context, m map[string]string) error {
return nil
},
},
store: &mocks.MockBatchKVStore[string, string]{
GetFunc: func(ctx context.Context, keys []string) (map[string]string, error) {
return map[string]string{"key2": "value2"}, nil
},
},
},
args: args[string]{context.Background(), []string{"key1", "key2"}},
want: map[string]string{
"key1": "value1",
"key2": "value2",
},
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.l.Get(tt.args.ctx, tt.args.keys)
if !tt.wantErr(t, err, fmt.Sprintf("Get(%v, %v)", tt.args.ctx, tt.args.keys)) {
return
}
assert.Equalf(t, tt.want, got, "Get(%v, %v)", tt.args.ctx, tt.args.keys)
})
}
}

func Test_batch_Set(t *testing.T) {
type args[K comparable, V any] struct {
ctx context.Context
kvs map[K]V
}
type testCase[K comparable, V any] struct {
name string
l batch[K, V]
args args[K, V]
wantErr assert.ErrorAssertionFunc
}
tests := []testCase[string, string]{
{
name: "store error",
l: batch[string, string]{
store: &mocks.MockBatchKVStore[string, string]{
SetFunc: func(ctx context.Context, m map[string]string) error {
return assert.AnError
},
},
},
args: args[string, string]{context.Background(), map[string]string{"key1": "value1"}},
wantErr: assert.Error,
}, {
name: "cache error",
l: batch[string, string]{
store: &mocks.MockBatchKVStore[string, string]{
SetFunc: func(ctx context.Context, m map[string]string) error {
return nil
},
},
cache: &mocks.MockBatchKVStore[string, string]{
DelFunc: func(ctx context.Context, keys []string) error {
return assert.AnError
},
},
},
args: args[string, string]{context.Background(), map[string]string{"key1": "value1"}},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.wantErr(t, tt.l.Set(tt.args.ctx, tt.args.kvs), fmt.Sprintf("Set(%v, %v)", tt.args.ctx, tt.args.kvs))
})
}
}

func Test_batch_Del(t *testing.T) {
type args[K comparable] struct {
ctx context.Context
keys []K
}
type testCase[K comparable, V any] struct {
name string
l batch[K, V]
args args[K]
wantErr assert.ErrorAssertionFunc
}
tests := []testCase[string, string]{
{
name: "store error",
l: batch[string, string]{
store: &mocks.MockBatchKVStore[string, string]{
DelFunc: func(ctx context.Context, keys []string) error {
return assert.AnError
},
},
},
args: args[string]{},
wantErr: assert.Error,
}, {
name: "cache error",
l: batch[string, string]{
store: &mocks.MockBatchKVStore[string, string]{
DelFunc: func(ctx context.Context, keys []string) error {
return nil
},
},
cache: &mocks.MockBatchKVStore[string, string]{
DelFunc: func(ctx context.Context, keys []string) error {
return assert.AnError
},
},
},
args: args[string]{},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.wantErr(t, tt.l.Del(tt.args.ctx, tt.args.keys), fmt.Sprintf("Del(%v, %v)", tt.args.ctx, tt.args.keys))
})
}
}
57 changes: 57 additions & 0 deletions layerkv/kv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package layerkv

import (
"context"
"errors"

"github.com/chenyanchen/db"
)

type layerKV[K comparable, V any] struct {
cache db.KV[K, V]
store db.KV[K, V]
}

func New[K comparable, V any](cache, store db.KV[K, V]) (*layerKV[K, V], error) {
if cache == nil {
return nil, errors.New("cache is nil")
}
if store == nil {
return nil, errors.New("store is nil")
}
return &layerKV[K, V]{cache: cache, store: store}, nil
}

func (l *layerKV[K, V]) Get(ctx context.Context, k K) (V, error) {
v, err := l.cache.Get(ctx, k)
if err == nil {
return v, nil
}

if !errors.Is(err, db.ErrNotFound) {
return v, err
}

v, err = l.store.Get(ctx, k)
if err != nil {
return v, err
}

return v, l.cache.Set(ctx, k, v)
}

func (l *layerKV[K, V]) Set(ctx context.Context, k K, v V) error {
if err := l.store.Set(ctx, k, v); err != nil {
return err
}

return l.cache.Del(ctx, k)
}

func (l *layerKV[K, V]) Del(ctx context.Context, k K) error {
if err := l.store.Del(ctx, k); err != nil {
return err
}

return l.cache.Del(ctx, k)
}
Loading

0 comments on commit 24c0996

Please sign in to comment.