Skip to content

Commit

Permalink
feat: add caching
Browse files Browse the repository at this point in the history
  • Loading branch information
Tuan Nguyen committed Feb 26, 2024
1 parent 4603fd8 commit a577d28
Show file tree
Hide file tree
Showing 39 changed files with 2,614 additions and 86 deletions.
15 changes: 11 additions & 4 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
with-expecter: true
packages:
github.com/kanthorlabs/common/logging:
github.com/kanthorlabs/common/cache:
config:
filename: "{{ .InterfaceName | snakecase }}.go"
dir: "mocks/{{ .PackageName }}"
mockname: "{{ .InterfaceName }}"
interfaces:
Logger:
github.com/kanthorlabs/common/timer:
Cache:
github.com/kanthorlabs/common/clock:
config:
filename: "{{ .InterfaceName | snakecase }}.go"
dir: "mocks/{{ .PackageName }}"
mockname: "{{ .InterfaceName }}"
interfaces:
Timer:
Clock:
github.com/kanthorlabs/common/logging:
config:
filename: "{{ .InterfaceName | snakecase }}.go"
dir: "mocks/{{ .PackageName }}"
mockname: "{{ .InterfaceName }}"
interfaces:
Logger:
github.com/kanthorlabs/common/passport:
config:
filename: "{{ .InterfaceName | snakecase }}.go"
Expand Down
17 changes: 17 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cache

import (
"context"
"time"

"github.com/kanthorlabs/common/patterns"
)

type Cache interface {
patterns.Connectable
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, entry any, ttl time.Duration) error
Exist(ctx context.Context, key string) bool
Del(ctx context.Context, key string) error
Expire(ctx context.Context, key string, at time.Time) error
}
7 changes: 7 additions & 0 deletions cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cache

import "github.com/kanthorlabs/common/cache/config"

var testconf = &config.Config{
Uri: "memory://",
}
13 changes: 13 additions & 0 deletions cache/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package config

import "github.com/kanthorlabs/common/validator"

type Config struct {
Uri string `json:"uri" yaml:"uri" mapstructure:"uri"`
}

func (conf *Config) Validate() error {
return validator.Validate(
validator.StringUri("CACHE.CONFIG.URI", conf.Uri),
)
}
14 changes: 14 additions & 0 deletions cache/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestConfig(t *testing.T) {
t.Run("KO", func(st *testing.T) {
conf := Config{}
require.ErrorContains(st, conf.Validate(), "CACHE.CONFIG.")
})
}
9 changes: 9 additions & 0 deletions cache/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package cache

import "errors"

var (
ErrAlreadyConnected = errors.New("CACHE.ALREADY_CONNECTED.ERROR")
ErrNotConnected = errors.New("CACHE.NOT_CONNECTED.ERROR")
ErrEntryNotFound = errors.New("CACHE.ENTRY.NOT_FOUND.ERROR")
)
129 changes: 129 additions & 0 deletions cache/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cache

import (
"context"
"encoding/json"
"errors"
"sync"
"time"

"github.com/jellydator/ttlcache/v3"
"github.com/kanthorlabs/common/cache/config"
"github.com/kanthorlabs/common/clock"
"github.com/kanthorlabs/common/logging"
"github.com/kanthorlabs/common/patterns"
)

func NewMemory(conf *config.Config, logger logging.Logger, watch clock.Clock) (Cache, error) {
if err := conf.Validate(); err != nil {
return nil, err
}

cache := ttlcache.New[string, []byte]()
return &memory{cache: cache, conf: conf, logger: logger, watch: watch}, nil
}

type memory struct {
conf *config.Config
logger logging.Logger
watch clock.Clock
cache *ttlcache.Cache[string, []byte]

mu sync.Mutex
status int
}

func (instance *memory) Connect(ctx context.Context) error {
instance.mu.Lock()
defer instance.mu.Unlock()

if instance.status == patterns.StatusConnected {
return ErrAlreadyConnected
}

go instance.cache.Start()

instance.status = patterns.StatusConnected
return nil
}

func (instance *memory) Readiness() error {
if instance.status == patterns.StatusDisconnected {
return nil
}
if instance.status != patterns.StatusConnected {
return ErrNotConnected
}

return nil
}

func (instance *memory) Liveness() error {
if instance.status == patterns.StatusDisconnected {
return nil
}
if instance.status != patterns.StatusConnected {
return ErrNotConnected
}

return nil
}

func (instance *memory) Disconnect(ctx context.Context) error {
instance.mu.Lock()
defer instance.mu.Unlock()

if instance.status != patterns.StatusConnected {
return ErrNotConnected
}
instance.status = patterns.StatusDisconnected

instance.cache.Stop()
instance.cache.DeleteAll()
return nil
}

func (instance *memory) Get(ctx context.Context, key string) ([]byte, error) {
item := instance.cache.Get(key)
if item == nil {
return nil, ErrEntryNotFound
}
return item.Value(), nil
}

func (instance *memory) Set(ctx context.Context, key string, entry any, ttl time.Duration) error {
var value []byte
var err error
if entry != nil {
value, err = json.Marshal(entry)
if err != nil {
return err
}
}
instance.cache.Set(key, value, ttl)
return nil
}

func (instance *memory) Exist(ctx context.Context, key string) bool {
return instance.cache.Has(key)
}

func (instance *memory) Del(ctx context.Context, key string) error {
instance.cache.Delete(key)
return nil
}

func (instance *memory) Expire(ctx context.Context, key string, at time.Time) error {
value, err := instance.Get(ctx, key)
if err != nil {
return err
}

ttl := at.Sub(instance.watch.Now())
if ttl < 0 {
return errors.New("CACHE.EXPIRE.EXPIRED_AT_TIME_PASS.ERROR")
}

instance.cache.Set(key, value, ttl)
return nil
}
152 changes: 152 additions & 0 deletions cache/memory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package cache

import (
"context"
"testing"
"time"

"github.com/google/uuid"
"github.com/kanthorlabs/common/cache/config"
"github.com/kanthorlabs/common/mocks/clock"
"github.com/kanthorlabs/common/testdata"
"github.com/kanthorlabs/common/testify"
"github.com/stretchr/testify/require"
)

func TestMemory(t *testing.T) {
t.Run("New", func(st *testing.T) {
st.Run("KO - configuration error", func(sst *testing.T) {
conf := &config.Config{}
_, err := NewMemory(conf, testify.Logger(), clock.NewClock(sst))
require.ErrorContains(st, err, "CACHE.CONFIG.")
})
})

t.Run(".Connect/.Readiness/.Liveness/.Disconnect", func(st *testing.T) {
c, err := NewMemory(testconf, testify.Logger(), clock.NewClock(st))
require.Nil(st, err)

require.ErrorIs(st, c.Readiness(), ErrNotConnected)
require.ErrorIs(st, c.Liveness(), ErrNotConnected)

require.Nil(st, c.Connect(context.Background()))

require.ErrorIs(st, c.Connect(context.Background()), ErrAlreadyConnected)

require.Nil(st, c.Readiness())
require.Nil(st, c.Liveness())

require.Nil(st, c.Disconnect(context.Background()))

require.Nil(st, c.Readiness())
require.Nil(st, c.Liveness())

require.ErrorIs(st, c.Disconnect(context.Background()), ErrNotConnected)
})

t.Run(".Get", func(st *testing.T) {
c, err := NewMemory(testconf, testify.Logger(), clock.NewClock(st))
require.Nil(st, err)
c.Connect(context.Background())
defer c.Disconnect(context.Background())

st.Run("OK", func(sst *testing.T) {
key := uuid.NewString()
err := c.Set(context.Background(), key, testdata.Fake.Blood().Name(), time.Hour)
require.Nil(st, err)

_, err = c.Get(context.Background(), key)
require.Nil(st, err)
})

st.Run("KO - not found error", func(sst *testing.T) {
key := uuid.NewString()
_, err := c.Get(context.Background(), key)
require.ErrorIs(st, err, ErrEntryNotFound)
})
})

t.Run(".Set", func(st *testing.T) {
c, err := NewMemory(testconf, testify.Logger(), clock.NewClock(st))
require.Nil(st, err)
c.Connect(context.Background())
defer c.Disconnect(context.Background())

st.Run("OK - not nil", func(sst *testing.T) {
key := uuid.NewString()
value := testdata.Fake.Blood().Name()
err := c.Set(context.Background(), key, value, time.Hour)
require.Nil(st, err)
})

st.Run("OK - nil", func(sst *testing.T) {
key := uuid.NewString()
err := c.Set(context.Background(), key, nil, time.Hour)
require.Nil(st, err)
})

st.Run("KO - marshal error", func(sst *testing.T) {
key := uuid.NewString()
err := c.Set(context.Background(), key, make(chan int), time.Hour)
require.ErrorContains(st, err, "json: unsupported type")
})
})

t.Run(".Exist", func(st *testing.T) {
c, err := NewMemory(testconf, testify.Logger(), clock.NewClock(st))
require.Nil(st, err)
c.Connect(context.Background())
defer c.Disconnect(context.Background())

st.Run("OK", func(sst *testing.T) {
key := uuid.NewString()
exist := c.Exist(context.Background(), key)
require.False(st, exist)
})
})

t.Run(".Del", func(st *testing.T) {
c, err := NewMemory(testconf, testify.Logger(), clock.NewClock(st))
require.Nil(st, err)
c.Connect(context.Background())
defer c.Disconnect(context.Background())

st.Run("OK", func(sst *testing.T) {
key := uuid.NewString()
err := c.Del(context.Background(), key)
require.Nil(st, err)
})
})

t.Run(".Expire", func(st *testing.T) {
watch := clock.NewClock(st)
c, err := NewMemory(testconf, testify.Logger(), watch)
require.Nil(st, err)
c.Connect(context.Background())
defer c.Disconnect(context.Background())

key := uuid.NewString()
err = c.Set(context.Background(), key, testdata.Fake.Blood().Name(), time.Hour)
require.Nil(st, err)

st.Run("OK", func(sst *testing.T) {
watch.EXPECT().Now().Return(time.Now().Add(-time.Hour)).Once()

err = c.Expire(context.Background(), key, time.Now())
require.Nil(st, err)
})

st.Run("KO - not found error", func(sst *testing.T) {
err = c.Expire(context.Background(), uuid.NewString(), time.Now())
require.ErrorIs(st, err, ErrEntryNotFound)
})

st.Run("KO - pass time", func(sst *testing.T) {
watch.EXPECT().Now().Return(time.Now().Add(time.Hour)).Once()

err = c.Expire(context.Background(), key, time.Now())
require.ErrorContains(st, err, "CACHE.EXPIRE.EXPIRED_AT_TIME_PASS.ERROR")
})
})

}
Loading

0 comments on commit a577d28

Please sign in to comment.