feat: implement lfu based cache strategy
All checks were successful
arcad/edge/pipeline/head This commit looks good
arcad/edge/pipeline/pr-master This commit looks good

This commit is contained in:
2024-01-06 16:48:14 +01:00
parent b9c08f647c
commit a276b92a03
29 changed files with 1858 additions and 165 deletions

View 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
View 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
}

View 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
}

View 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
View 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]{}

View File

@ -0,0 +1,2 @@
*
!.gitignore