feat: implement lfu based cache strategy
This commit is contained in:
37
pkg/storage/driver/cache/lfu/fs/cache_test.go
vendored
Normal file
37
pkg/storage/driver/cache/lfu/fs/cache_test.go
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/testsuite"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestCacheWithFSStore(t *testing.T) {
|
||||
testsuite.TestCacheWithStore(t, func(testName string) lfu.Store[string, string] {
|
||||
dir := filepath.Join("testdata", "testsuite", testName)
|
||||
store := NewStore[string, string](dir,
|
||||
WithMarshalValue[string, string](func(value string) (io.Reader, error) {
|
||||
return bytes.NewBuffer([]byte(value)), nil
|
||||
}),
|
||||
WithUnmarshalValue[string, string](func(r io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}),
|
||||
)
|
||||
|
||||
if err := store.Clear(); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return store
|
||||
})
|
||||
}
|
19
pkg/storage/driver/cache/lfu/fs/hash.go
vendored
Normal file
19
pkg/storage/driver/cache/lfu/fs/hash.go
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func DefaultGetPath[K comparable](key K) ([]string, error) {
|
||||
uintHash, err := hashstructure.Hash(key, hashstructure.FormatV2, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
hash := strconv.FormatUint(uintHash, 16)
|
||||
|
||||
return []string{hash}, nil
|
||||
}
|
31
pkg/storage/driver/cache/lfu/fs/marshal.go
vendored
Normal file
31
pkg/storage/driver/cache/lfu/fs/marshal.go
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func DefaultMarshalValue[V any](value V) (io.Reader, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := gob.NewEncoder(&buf)
|
||||
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
func DefaultUnmarshalValue[V any](d io.Reader) (V, error) {
|
||||
var value V
|
||||
encoder := gob.NewDecoder(d)
|
||||
|
||||
if err := encoder.Decode(&value); err != nil {
|
||||
return *new(V), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
45
pkg/storage/driver/cache/lfu/fs/options.go
vendored
Normal file
45
pkg/storage/driver/cache/lfu/fs/options.go
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
package fs
|
||||
|
||||
import "io"
|
||||
|
||||
type GetPathFunc[K comparable] func(key K) ([]string, error)
|
||||
type MarshalValueFunc[V any] func(value V) (io.Reader, error)
|
||||
type UnmarshalValueFunc[V any] func(r io.Reader) (V, error)
|
||||
|
||||
type Options[K comparable, V any] struct {
|
||||
GetPath GetPathFunc[K]
|
||||
MarshalValue MarshalValueFunc[V]
|
||||
UnmarshalValue UnmarshalValueFunc[V]
|
||||
}
|
||||
|
||||
type OptionsFunc[K comparable, V any] func(opts *Options[K, V])
|
||||
|
||||
func DefaultOptions[K comparable, V any](funcs ...OptionsFunc[K, V]) *Options[K, V] {
|
||||
opts := &Options[K, V]{
|
||||
GetPath: DefaultGetPath[K],
|
||||
MarshalValue: DefaultMarshalValue[V],
|
||||
UnmarshalValue: DefaultUnmarshalValue[V],
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithGetPath[K comparable, V any](getKeyHash GetPathFunc[K]) OptionsFunc[K, V] {
|
||||
return func(opts *Options[K, V]) {
|
||||
opts.GetPath = getKeyHash
|
||||
}
|
||||
}
|
||||
|
||||
func WithMarshalValue[K comparable, V any](marshalValue MarshalValueFunc[V]) OptionsFunc[K, V] {
|
||||
return func(opts *Options[K, V]) {
|
||||
opts.MarshalValue = marshalValue
|
||||
}
|
||||
}
|
||||
|
||||
func WithUnmarshalValue[K comparable, V any](unmarshalValue UnmarshalValueFunc[V]) OptionsFunc[K, V] {
|
||||
return func(opts *Options[K, V]) {
|
||||
opts.UnmarshalValue = unmarshalValue
|
||||
}
|
||||
}
|
165
pkg/storage/driver/cache/lfu/fs/store.go
vendored
Normal file
165
pkg/storage/driver/cache/lfu/fs/store.go
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Store[K comparable, V any] struct {
|
||||
baseDir string
|
||||
getPath GetPathFunc[K]
|
||||
marshalValue MarshalValueFunc[V]
|
||||
unmarshalValue UnmarshalValueFunc[V]
|
||||
}
|
||||
|
||||
// Delete implements Store.
|
||||
func (s *Store[K, V]) Delete(key K) error {
|
||||
path, err := s.getEntryPath(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements Store.
|
||||
func (s *Store[K, V]) Get(key K) (V, error) {
|
||||
path, err := s.getEntryPath(key)
|
||||
if err != nil {
|
||||
return *new(V), errors.WithStack(err)
|
||||
}
|
||||
|
||||
value, err := s.readValue(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return *new(V), errors.WithStack(lfu.ErrNotFound)
|
||||
}
|
||||
|
||||
return *new(V), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Set implements Store.
|
||||
func (s *Store[K, V]) Set(key K, value V) error {
|
||||
path, err := s.getEntryPath(key)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.writeValue(path, value); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store[K, V]) Clear() error {
|
||||
if err := os.RemoveAll(s.baseDir); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store[K, V]) getEntryPath(k K) (string, error) {
|
||||
path, err := s.getPath(k)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
path = append([]string{s.baseDir}, path...)
|
||||
return filepath.Join(path...), nil
|
||||
}
|
||||
|
||||
func (s *Store[K, V]) writeValue(path string, value V) error {
|
||||
fi, err := os.Stat(path)
|
||||
if err == nil && !fi.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s already exists and is not a regular file", path)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp(dir, filepath.Base(path)+".tmp")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
tmpName := f.Name()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
|
||||
reader, err := s.marshalValue(value)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.Sync(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store[K, V]) readValue(path string) (V, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return *new(V), errors.WithStack(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}()
|
||||
|
||||
value, err := s.unmarshalValue(file)
|
||||
|
||||
if err != nil {
|
||||
return *new(V), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func NewStore[K comparable, V any](baseDir string, funcs ...OptionsFunc[K, V]) *Store[K, V] {
|
||||
opts := DefaultOptions[K, V](funcs...)
|
||||
return &Store[K, V]{
|
||||
baseDir: baseDir,
|
||||
getPath: opts.GetPath,
|
||||
unmarshalValue: opts.UnmarshalValue,
|
||||
marshalValue: opts.MarshalValue,
|
||||
}
|
||||
}
|
||||
|
||||
var _ lfu.Store[string, int] = &Store[string, int]{}
|
2
pkg/storage/driver/cache/lfu/fs/testdata/.gitignore
vendored
Normal file
2
pkg/storage/driver/cache/lfu/fs/testdata/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
Reference in New Issue
Block a user