diff options
author | Marc Pervaz Boocha <marcpervaz@qburst.com> | 2025-02-19 18:28:03 +0530 |
---|---|---|
committer | Marc Pervaz Boocha <marcpervaz@qburst.com> | 2025-02-19 18:28:03 +0530 |
commit | 893b439ccb9511ed4a5595bdf8048bb637da1200 (patch) | |
tree | 5885be596e283a719166ba6af9339c3095bc471d | |
parent | Bootstraped code for housekeeping operations (diff) | |
download | cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar.gz cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar.bz2 cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar.lz cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar.xz cache-893b439ccb9511ed4a5595bdf8048bb637da1200.tar.zst cache-893b439ccb9511ed4a5595bdf8048bb637da1200.zip |
Added Eviction and Options setup
-rw-r--r-- | conn.go | 170 | ||||
-rw-r--r-- | encoding.go | 96 | ||||
-rw-r--r-- | encoding_test.go | 150 | ||||
-rw-r--r-- | evict.go | 123 | ||||
-rw-r--r-- | evict_test.go | 295 | ||||
-rw-r--r-- | map.go | 204 | ||||
-rw-r--r-- | map_test.go | 4 | ||||
-rw-r--r-- | utils.go | 6 |
8 files changed, 794 insertions, 254 deletions
@@ -10,124 +10,182 @@ import ( "github.com/vmihailenco/msgpack" ) -type DB[K any, V any] struct { - file io.WriteSeeker - Store Store - snapshotTicker *pauseTimer - cleanupTicker *pauseTimer - stop chan struct{} +type db struct { + File io.WriteSeeker + Store store + SnapshotTicker *pauseTimer + CleanupTicker *pauseTimer + Stop chan struct{} wg sync.WaitGroup } -func OpenFile[K any, V any](filename string) (*DB[K, V], error) { - ret := OpenMem[K, V]() - file, err := os.OpenFile(filename, os.O_RDWR, 0) - if errors.Is(err, os.ErrNotExist) { - file, err := os.Create(filename) +type Option func(*db) error + +func openFile(filename string, options ...Option) (*db, error) { + ret, err := openMem(options...) + if err != nil { + return nil, err + } + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + if fileInfo.Size() == 0 { + ret.File = file + ret.Flush() + } else { + err := ret.Store.LoadSnapshot(file) if err != nil { return nil, err } - ret.file = file - ret.Flush() - } else if err == nil { - ret.Store.LoadSnapshot(file) - ret.file = file - } else { - return nil, err + ret.File = file } return ret, nil } -func OpenMem[K any, V any]() *DB[K, V] { - ret := &DB[K, V]{ - snapshotTicker: newPauseTimer(0), +func openMem(options ...Option) (*db, error) { + ret := &db{ + SnapshotTicker: newPauseTimerStopped(0), + CleanupTicker: newPauseTimerStopped(10 * time.Second), } - ret.snapshotTicker.Stop() - ret.Clear() - ret.Store.strategy.evict = &ret.Store.evict - ret.SetStratergy(StrategyNone) - return ret + ret.Store.Init() + ret.SetConfig(options...) + return ret, nil } -func (d *DB[K, V]) Start() { - d.stop = make(chan struct{}) +func (d *db) Start() { + d.Stop = make(chan struct{}) d.wg.Add(1) go d.backgroundWorker() } -func (d *DB[K, V]) SetStratergy(e EvictionPolicyType) error { - return d.Store.strategy.SetPolicy(e) +func (d *db) SetConfig(options ...Option) error { + for _, opt := range options { + if err := opt(d); err != nil { + return err + } + } + return nil } -func (d *DB[K, V]) SetMaxCost(e EvictionPolicyType) { - d.Store.maxCost = d.Store.maxCost +func WithPolicy(e EvictionPolicyType) Option { + return func(d *db) error { + return d.Store.Policy.SetPolicy(e) + } } -func (d *DB[K, V]) SetSnapshotTime(t time.Duration) { - d.snapshotTicker.Reset(t) +func WithMaxCost(maxCost uint64) Option { + return func(d *db) error { + d.Store.MaxCost = maxCost + return nil + } +} + +func SetSnapshotTime(t time.Duration) Option { + return func(d *db) error { + d.SnapshotTicker.Reset(t) + return nil + } +} + +func SetCleanupTime(t time.Duration) Option { + return func(d *db) error { + d.CleanupTicker.Reset(t) + return nil + } } -func (d *DB[K, V]) backgroundWorker() { +func (d *db) backgroundWorker() { defer d.wg.Done() - d.snapshotTicker.Resume() - defer d.snapshotTicker.Stop() + d.SnapshotTicker.Resume() + defer d.SnapshotTicker.Stop() + + d.CleanupTicker.Resume() + defer d.CleanupTicker.Stop() for { select { - case <-d.stop: + case <-d.Stop: return - case <-d.snapshotTicker.C: - d.Flush() - case <-d.snapshotTicker.C: + case <-d.SnapshotTicker.C: d.Flush() + case <-d.CleanupTicker.C: + cleanup(&d.Store) + evict(&d.Store) } } } -func (d *DB[K, V]) Close() { - close(d.stop) +func (d *db) Close() { + close(d.Stop) d.wg.Wait() d.Flush() d.Clear() - if d.file != nil { - closer, ok := d.file.(io.Closer) + if d.File != nil { + closer, ok := d.File.(io.Closer) if ok { closer.Close() } } } -func (d *DB[K, V]) Flush() error { - if d.file != nil { - return d.Store.Snapshot(d.file) +func (d *db) Flush() error { + if d.File != nil { + return d.Store.Snapshot(d.File) } return nil } -func (d *DB[K, V]) Clear() { +func (d *db) Clear() { d.Store.Clear() } var ErrKeyNotFound = errors.New("key not found") -func (h *DB[K, V]) Get(key K) (V, time.Duration, error) { +// The Cache database. Can be initialized by either OpenFile or OpenMem. Uses per DB Locks. +type DB[K any, V any] struct { + *db +} + +func OpenFile[K any, V any](filename string, options ...Option) (DB[K, V], error) { + ret, err := openFile(filename, options...) + if err != nil { + return zero[DB[K, V]](), err + } + ret.Start() + return DB[K, V]{db: ret}, nil +} + +func OpenMem[K any, V any](filename string, options ...Option) (DB[K, V], error) { + ret, err := openMem(options...) + if err != nil { + return zero[DB[K, V]](), err + } + ret.Start() + return DB[K, V]{db: ret}, nil +} + +func (h *DB[K, V]) Get(key K, value V) (V, time.Duration, error) { keyData, err := msgpack.Marshal(key) if err != nil { - return zero[V](), 0, err + return value, 0, err } v, ttl, ok := h.Store.Get(keyData) if !ok { - return zero[V](), 0, ErrKeyNotFound + return value, 0, ErrKeyNotFound } - var ret V - if err = msgpack.Unmarshal(v, &ret); err != nil { - return zero[V](), 0, err + if err = msgpack.Unmarshal(v, value); err != nil { + return value, 0, err } - return ret, ttl, err + return value, ttl, err } func (h *DB[K, V]) Set(key K, value V, ttl time.Duration) error { diff --git a/encoding.go b/encoding.go index bd2d4b7..09645ab 100644 --- a/encoding.go +++ b/encoding.go @@ -7,38 +7,42 @@ import ( "time" ) -type Encoder struct { - *bufio.Writer +type encoder struct { + w *bufio.Writer buf []byte } -func NewEncoder(w io.Writer) *Encoder { - return &Encoder{ - Writer: bufio.NewWriter(w), - buf: make([]byte, 8), +func newEncoder(w io.Writer) *encoder { + return &encoder{ + w: bufio.NewWriter(w), + buf: make([]byte, 8), } } -func (e *Encoder) EncodeUint64(val uint64) error { +func (e *encoder) Flush() error { + return e.w.Flush() +} + +func (e *encoder) EncodeUint64(val uint64) error { binary.LittleEndian.PutUint64(e.buf, val) - _, err := e.Write(e.buf) + _, err := e.w.Write(e.buf) return err } -func (e *Encoder) EncodeTime(val time.Time) error { +func (e *encoder) EncodeTime(val time.Time) error { return e.EncodeUint64(uint64(val.Unix())) } -func (e *Encoder) EncodeBytes(val []byte) error { +func (e *encoder) EncodeBytes(val []byte) error { if err := e.EncodeUint64(uint64(len(val))); err != nil { return err } - _, err := e.Write(val) + _, err := e.w.Write(val) return err } -func (e *Encoder) EncodeNode(n *Node) error { +func (e *encoder) EncodeNode(n *node) error { if err := e.EncodeUint64(n.Hash); err != nil { return err } @@ -62,27 +66,27 @@ func (e *Encoder) EncodeNode(n *Node) error { return nil } -type Decoder struct { - *bufio.Reader +type decoder struct { + r *bufio.Reader buf []byte } -func NewDecoder(r io.Reader) *Decoder { - return &Decoder{ - Reader: bufio.NewReader(r), - buf: make([]byte, 8), +func newDecoder(r io.Reader) *decoder { + return &decoder{ + r: bufio.NewReader(r), + buf: make([]byte, 8), } } -func (d *Decoder) DecodeUint64() (uint64, error) { - _, err := io.ReadFull(d, d.buf) +func (d *decoder) DecodeUint64() (uint64, error) { + _, err := io.ReadFull(d.r, d.buf) if err != nil { return 0, err } return binary.LittleEndian.Uint64(d.buf), nil } -func (d *Decoder) DecodeTime() (time.Time, error) { +func (d *decoder) DecodeTime() (time.Time, error) { ts, err := d.DecodeUint64() if err != nil { return zero[time.Time](), err @@ -90,18 +94,18 @@ func (d *Decoder) DecodeTime() (time.Time, error) { return time.Unix(int64(ts), 0), nil } -func (d *Decoder) DecodeBytes() ([]byte, error) { +func (d *decoder) DecodeBytes() ([]byte, error) { lenVal, err := d.DecodeUint64() if err != nil { return nil, err } data := make([]byte, lenVal) - _, err = io.ReadFull(d, data) + _, err = io.ReadFull(d.r, data) return data, err } -func (d *Decoder) DecodeNodes() (*Node, error) { - n := &Node{} +func (d *decoder) DecodeNodes() (*node, error) { + n := &node{} hash, err := d.DecodeUint64() if err != nil { @@ -133,63 +137,63 @@ func (d *Decoder) DecodeNodes() (*Node, error) { return n, err } -func (s *Store) Snapshot(w io.WriteSeeker) error { - s.mu.RLock() - defer s.mu.RUnlock() +func (s *store) Snapshot(w io.WriteSeeker) error { + s.mu.Lock() + defer s.mu.Unlock() w.Seek(0, io.SeekStart) - wr := NewEncoder(w) + wr := newEncoder(w) - wr.EncodeUint64(s.maxCost) - wr.EncodeUint64(uint64(s.strategy.Type)) - wr.EncodeUint64(s.lenght) + wr.EncodeUint64(s.MaxCost) + wr.EncodeUint64(uint64(s.Policy.Type)) + wr.EncodeUint64(s.Lenght) - for v := s.evict.EvictNext; v != &s.evict; v = v.EvictNext { + for v := s.Evict.EvictNext; v != &s.Evict; v = v.EvictNext { if err := wr.EncodeNode(v); err != nil { return err } } - wr.Flush() + wr.w.Flush() return nil } -func (s *Store) LoadSnapshot(r io.ReadSeeker) error { +func (s *store) LoadSnapshot(r io.ReadSeeker) error { r.Seek(0, io.SeekStart) - rr := NewDecoder(r) + rr := newDecoder(r) maxCost, err := rr.DecodeUint64() if err != nil { return err } - s.maxCost = maxCost + s.MaxCost = maxCost policy, err := rr.DecodeUint64() if err != nil { return err } - s.strategy.SetPolicy(EvictionPolicyType(policy)) + s.Policy.SetPolicy(EvictionPolicyType(policy)) lenght, err := rr.DecodeUint64() if err != nil { return err } - s.lenght = lenght + s.Lenght = lenght k := 128 - for k < int(s.lenght) { + for k < int(s.Lenght) { k = k << 1 } - s.bucket = make([]Node, k) - for range s.lenght { + s.Bucket = make([]node, k) + for range s.Lenght { v, err := rr.DecodeNodes() if err != nil { return err } - idx := v.Hash % uint64(len(s.bucket)) + idx := v.Hash % uint64(len(s.Bucket)) - bucket := &s.bucket[idx] + bucket := &s.Bucket[idx] lazyInitBucket(bucket) v.HashPrev = bucket @@ -197,12 +201,12 @@ func (s *Store) LoadSnapshot(r io.ReadSeeker) error { v.HashNext.HashPrev = v v.HashPrev.HashNext = v - v.EvictPrev = &s.evict + v.EvictPrev = &s.Evict v.EvictNext = v.EvictPrev.EvictNext v.EvictNext.EvictPrev = v v.EvictPrev.EvictNext = v - s.cost = s.cost + uint64(len(v.Key)) + uint64(len(v.Value)) + s.Cost = s.Cost + uint64(len(v.Key)) + uint64(len(v.Value)) } return nil } diff --git a/encoding_test.go b/encoding_test.go new file mode 100644 index 0000000..d48d2fd --- /dev/null +++ b/encoding_test.go @@ -0,0 +1,150 @@ +package cache + +import ( + "bytes" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeDecodeUint64(t *testing.T) { + tests := []struct { + name string + value uint64 + }{ + {name: "Positive", value: 1234567890}, + {name: "Zero", value: 0}, + {name: "Max", value: ^uint64(0)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + e := newEncoder(&buf) + + err := e.EncodeUint64(tt.value) + assert.NoError(t, err) + err = e.Flush() + assert.NoError(t, err) + + decoder := newDecoder(bytes.NewReader(buf.Bytes())) + + decodedValue, err := decoder.DecodeUint64() + assert.NoError(t, err) + + assert.Equal(t, tt.value, decodedValue) + }) + } +} + +func TestEncodeDecodeTime(t *testing.T) { + tests := []struct { + name string + value time.Time + }{ + {name: "Time Now", value: time.Now()}, + {name: "Time Zero", value: time.Time{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + e := newEncoder(&buf) + + err := e.EncodeTime(tt.value) + assert.NoError(t, err) + err = e.Flush() + assert.NoError(t, err) + + decoder := newDecoder(bytes.NewReader(buf.Bytes())) + + decodedValue, err := decoder.DecodeTime() + assert.NoError(t, err) + + assert.WithinDuration(t, tt.value, decodedValue, time.Second) + }) + } +} + +func TestEncodeDecodeBytes(t *testing.T) { + tests := []struct { + name string + value []byte + }{ + {name: "Empty", value: []byte{}}, + {name: "Non-Empty", value: []byte("hello world")}, + {name: "Bytes Large", value: []byte("A very long string of characters to test large data")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + e := newEncoder(&buf) + + err := e.EncodeBytes(tt.value) + assert.NoError(t, err) + err = e.Flush() + assert.NoError(t, err) + + decoder := newDecoder(bytes.NewReader(buf.Bytes())) + + decodedValue, err := decoder.DecodeBytes() + assert.NoError(t, err) + + assert.Equal(t, tt.value, decodedValue) + }) + } +} + +func TestEncodeDecodeNode(t *testing.T) { + tests := []struct { + name string + value *node + }{ + {name: "Empty", value: &node{ + Hash: 1234567890, + Expiration: time.Now(), + Access: 987654321, + Key: []byte("testKey"), + Value: []byte("testValue"), + }}, + {name: "Non-Empty", value: &node{ + Hash: 1234567890, + Expiration: time.Now(), + Access: 987654321, + Key: []byte("testKey"), + Value: []byte("testValue"), + }}, + {name: "Bytes Large", value: &node{ + Hash: 1234567890, + Expiration: time.Now(), + Access: 987654321, + Key: []byte("testKey"), + Value: []byte("testValue"), + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + e := newEncoder(&buf) + + err := e.EncodeNode(tt.value) + assert.NoError(t, err) + err = e.Flush() + assert.NoError(t, err) + + decoder := newDecoder(bytes.NewReader(buf.Bytes())) + + decodedValue, err := decoder.DecodeNodes() + assert.NoError(t, err) + + assert.Equal(t, tt.value.Hash, decodedValue.Hash) + assert.WithinDuration(t, tt.value.Expiration, decodedValue.Expiration, 1*time.Second) + assert.Equal(t, tt.value.Access, decodedValue.Access) + assert.Equal(t, tt.value.Key, decodedValue.Key) + assert.Equal(t, tt.value.Value, decodedValue.Value) + }) + } +} @@ -5,88 +5,100 @@ import "errors" type EvictionPolicyType int const ( - StrategyNone EvictionPolicyType = iota - StrategyFIFO - StrategyLRU - StrategyLFU + PolicyNone EvictionPolicyType = iota + PolicyFIFO + PolicyLRU + PolicyLFU + PolicyLTR ) -type EvictionStrategies interface { - OnInsert(n *Node) - OnAccess(n *Node) - Evict() *Node +type evictionStrategies interface { + OnInsert(n *node) + OnAccess(n *node) + Evict() *node } -type EvictionPolicy struct { - EvictionStrategies +type evictionPolicy struct { + evictionStrategies Type EvictionPolicyType - evict *Node + evict *node } -func (e *EvictionPolicy) SetPolicy(y EvictionPolicyType) error { - store := map[EvictionPolicyType]func() EvictionStrategies{ - StrategyNone: func() EvictionStrategies { - return NoneStrategies{evict: e.evict} +func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error { + store := map[EvictionPolicyType]func() evictionStrategies{ + PolicyNone: func() evictionStrategies { + return nonePolicy{evict: e.evict} }, - StrategyFIFO: func() EvictionStrategies { - return FIFOStrategies{evict: e.evict} + PolicyFIFO: func() evictionStrategies { + return fifoPolicy{evict: e.evict} }, - StrategyLRU: func() EvictionStrategies { - return LRUStrategies{evict: e.evict} + PolicyLRU: func() evictionStrategies { + return lruPolicy{evict: e.evict} + }, + PolicyLFU: func() evictionStrategies { + return lfuPolicy{evict: e.evict} }, } factory, ok := store[y] if !ok { - return errors.New("Invalid Policy") + return errors.New("invalid olicy") } - e.EvictionStrategies = factory() + e.evictionStrategies = factory() return nil } -type NoneStrategies struct { - evict *Node +type nonePolicy struct { + evict *node } -func (s NoneStrategies) OnInsert(node *Node) { +func (s nonePolicy) OnInsert(node *node) { node.EvictPrev = s.evict node.EvictNext = node.EvictPrev.EvictNext node.EvictNext.EvictPrev = node node.EvictPrev.EvictNext = node } -func (NoneStrategies) OnAccess(n *Node) { +func (nonePolicy) OnAccess(n *node) { } -func (NoneStrategies) Evict() *Node { +func (nonePolicy) Evict() *node { return nil } -type FIFOStrategies struct { - evict *Node +type fifoPolicy struct { + evict *node } -func (s FIFOStrategies) OnInsert(node *Node) { +func (s fifoPolicy) OnInsert(node *node) { node.EvictPrev = s.evict node.EvictNext = node.EvictPrev.EvictNext node.EvictNext.EvictPrev = node node.EvictPrev.EvictNext = node } -func (FIFOStrategies) OnAccess(n *Node) { +func (fifoPolicy) OnAccess(n *node) { } -func (s FIFOStrategies) Evict() *Node { - return s.evict.EvictPrev +func (s fifoPolicy) Evict() *node { + if s.evict.EvictPrev != s.evict { + return s.evict.EvictPrev + } else { + return nil + } } -type LRUStrategies struct { - evict *Node +type lruPolicy struct { + evict *node } -func (s LRUStrategies) OnInsert(node *Node) { +func (s lruPolicy) OnInsert(node *node) { + node.EvictPrev = s.evict + node.EvictNext = node.EvictPrev.EvictNext + node.EvictNext.EvictPrev = node + node.EvictPrev.EvictNext = node } -func (s LRUStrategies) OnAccess(node *Node) { +func (s lruPolicy) OnAccess(node *node) { node.EvictNext.EvictPrev = node.EvictPrev node.EvictPrev.EvictNext = node.EvictNext @@ -96,36 +108,43 @@ func (s LRUStrategies) OnAccess(node *Node) { node.EvictPrev.EvictNext = node } -func (s LRUStrategies) Evict() *Node { - return s.evict.EvictPrev +func (s lruPolicy) Evict() *node { + if s.evict.EvictPrev != s.evict { + return s.evict.EvictPrev + } else { + return nil + } } -type LFUStrategies struct { - evict *Node +type lfuPolicy struct { + evict *node } -func (s LFUStrategies) OnInsert(node *Node) { +func (s lfuPolicy) OnInsert(node *node) { node.EvictPrev = s.evict node.EvictNext = node.EvictPrev.EvictNext node.EvictNext.EvictPrev = node node.EvictPrev.EvictNext = node + node.Access = 0 } -func (s LFUStrategies) OnAccess(node *Node) { - node.Access += 1 +func (s lfuPolicy) OnAccess(node *node) { + node.Access++ - node.EvictNext.EvictPrev = node.EvictPrev - node.EvictPrev.EvictNext = node.EvictNext + for v := node.EvictPrev; v.EvictPrev != s.evict; v = v.EvictPrev { + if v.Access <= node.Access { + node.EvictNext.EvictPrev = node.EvictPrev + node.EvictPrev.EvictNext = node.EvictNext - for v := node.EvictPrev; v != s.evict; v = v.EvictPrev { - if v.Access >= node.Access { node.EvictPrev = v node.EvictNext = node.EvictPrev.EvictNext node.EvictNext.EvictPrev = node node.EvictPrev.EvictNext = node - break + return } } + node.EvictNext.EvictPrev = node.EvictPrev + node.EvictPrev.EvictNext = node.EvictNext node.EvictPrev = s.evict node.EvictNext = node.EvictPrev.EvictNext @@ -133,6 +152,10 @@ func (s LFUStrategies) OnAccess(node *Node) { node.EvictPrev.EvictNext = node } -func (s LFUStrategies) Evict() *Node { - return s.evict.EvictPrev +func (s lfuPolicy) Evict() *node { + if s.evict.EvictPrev != s.evict { + return s.evict.EvictPrev + } else { + return nil + } } diff --git a/evict_test.go b/evict_test.go new file mode 100644 index 0000000..3503be4 --- /dev/null +++ b/evict_test.go @@ -0,0 +1,295 @@ +package cache + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func createSentinel(t testing.TB) *node { + t.Helper() + n1 := &node{Key: []byte("Sentinel")} + n1.EvictNext = n1 + n1.EvictPrev = n1 + return n1 +} + +func getListOrder(t testing.TB, evict *node) []*node { + t.Helper() + + var order []*node + current := evict.EvictNext + for current != evict { + order = append(order, current) + current = current.EvictNext + } + for _, n := range order { + assert.Same(t, n, n.EvictPrev.EvictNext) + } + return order +} + +func TestNonePolicy(t *testing.T) { + t.Parallel() + + t.Run("OnInsert", func(t *testing.T) { + t.Parallel() + + policy := nonePolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Contains(t, order, n0) + assert.Contains(t, order, n1) + }) + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + policy := nonePolicy{evict: createSentinel(t)} + + policy.OnInsert(&node{}) + + assert.Nil(t, policy.Evict()) + }) +} + +func TestFIFOPolicy(t *testing.T) { + t.Parallel() + + t.Run("OnInsert", func(t *testing.T) { + t.Parallel() + + policy := fifoPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, order[0], n1) + assert.Same(t, order[1], n0) + }) + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + policy := fifoPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + evictedNode := policy.Evict() + assert.Same(t, n0, evictedNode) + }) + t.Run("Empty List", func(t *testing.T) { + t.Parallel() + + policy := fifoPolicy{evict: createSentinel(t)} + + assert.Nil(t, policy.Evict()) + }) + }) +} + +func TestLRUPolicy(t *testing.T) { + t.Run("OnInsert", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, order[0], n1) + assert.Same(t, order[1], n0) + }) + + t.Run("OnAccess", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + policy.OnAccess(n0) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, order[0], n0) + assert.Same(t, order[1], n1) + }) + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + evictedNode := policy.Evict() + assert.Same(t, n0, evictedNode) + }) + + t.Run("OnAccess End", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + policy.OnAccess(n0) + + evictedNode := policy.Evict() + assert.Same(t, n1, evictedNode) + }) + + t.Run("OnAccess Interleaved", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnAccess(n0) + policy.OnInsert(n1) + + evictedNode := policy.Evict() + assert.Same(t, n0, evictedNode) + }) + + t.Run("Empty", func(t *testing.T) { + t.Parallel() + + policy := lruPolicy{evict: createSentinel(t)} + + assert.Nil(t, policy.Evict()) + }) + }) +} + +func TestLFUPolicy(t *testing.T) { + t.Parallel() + + t.Run("OnInsert", func(t *testing.T) { + t.Parallel() + + policy := lfuPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Contains(t, order, n0) + assert.Contains(t, order, n1) + }) + + t.Run("OnAccess", func(t *testing.T) { + t.Parallel() + + policy := lfuPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + policy.OnAccess(n0) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, order[0], n0) + assert.Same(t, order[1], n1) + }) + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + policy := lfuPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("0")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + policy.OnAccess(n0) + + evictedNode := policy.Evict() + assert.Same(t, n1, evictedNode) + }) + + t.Run("Evict After Multiple Accesses", func(t *testing.T) { + t.Parallel() + + policy := lfuPolicy{evict: createSentinel(t)} + + n0 := &node{Key: []byte("1")} + n1 := &node{Key: []byte("1")} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + policy.OnAccess(n0) + + policy.OnAccess(n1) + policy.OnAccess(n1) + + evictedNode := policy.Evict() + assert.Same(t, n0, evictedNode) + }) + + t.Run("Empty List", func(t *testing.T) { + t.Parallel() + + policy := lfuPolicy{evict: createSentinel(t)} + + assert.Nil(t, policy.Evict()) + }) + }) +} @@ -2,104 +2,115 @@ package cache import ( "bytes" - "iter" "sync" "time" ) -type Map[K any, V any] interface { - Set(key K, value V, ttl time.Duration) error - Get(key K) (V, time.Duration, error) - Delete(key K) error - Clear() iter.Seq2[K, V] -} - -type Node struct { +type node struct { Hash uint64 Expiration time.Time Access uint64 Key []byte Value []byte - HashNext *Node - HashPrev *Node - EvictNext *Node - EvictPrev *Node + HashNext *node + HashPrev *node + EvictNext *node + EvictPrev *node } -func (n *Node) IsValid() bool { +func (n *node) IsValid() bool { return n.Expiration.IsZero() || n.Expiration.After(time.Now()) } -func (n *Node) Detach() { +func (n *node) TTL() time.Duration { + if n.Expiration.IsZero() { + return 0 + } else { + return time.Until(n.Expiration) + } } -type Store struct { - bucket []Node - lenght uint64 - cost uint64 - evict Node - maxCost uint64 - strategy EvictionPolicy - mu sync.RWMutex +type store struct { + Bucket []node + Lenght uint64 + Cost uint64 + Evict node + MaxCost uint64 + Policy evictionPolicy + mu sync.Mutex } -func (s *Store) Init() { +func (s *store) Init() { s.Clear() - s.strategy.evict = &s.evict - s.strategy.SetPolicy(StrategyNone) + s.Policy.evict = &s.Evict + s.Policy.SetPolicy(PolicyNone) } -func (s *Store) Clear() { +func (s *store) Clear() { s.mu.Lock() defer s.mu.Unlock() - s.bucket = make([]Node, 8) - s.lenght = 0 - s.cost = 0 + s.Bucket = make([]node, 8) + s.Lenght = 0 + s.Cost = 0 - s.evict.EvictNext = &s.evict - s.evict.EvictPrev = &s.evict + s.Evict.EvictNext = &s.Evict + s.Evict.EvictPrev = &s.Evict } -func lookup(s *Store, key []byte) (uint64, uint64) { +func lookup(s *store, key []byte) (uint64, uint64) { hash := hash(key) - return hash % uint64(len(s.bucket)), hash + return hash % uint64(len(s.Bucket)), hash } -func lazyInitBucket(n *Node) { +func lazyInitBucket(n *node) { if n.HashNext == nil { n.HashNext = n n.HashPrev = n } } -func (s *Store) Get(key []byte) ([]byte, time.Duration, bool) { - s.mu.RLock() - defer s.mu.RUnlock() - - idx, _ := lookup(s, key) +func (s *store) lookup(key []byte) (*node, uint64, uint64) { + idx, hash := lookup(s, key) - bucket := &s.bucket[idx] + bucket := &s.Bucket[idx] lazyInitBucket(bucket) for v := bucket.HashNext; v != bucket; v = v.HashNext { if bytes.Equal(key, v.Key) { - if !v.IsValid() { - return nil, 0, false - } - s.strategy.OnAccess(v) - return v.Value, time.Until(v.Expiration), true + return v, idx, hash + } + } + + return nil, idx, hash +} + +func (s *store) get(key []byte) ([]byte, time.Duration, bool) { + v, _, _ := s.lookup(key) + if v != nil { + if !v.IsValid() { + deleteNode(s, v) + return nil, 0, false } + s.Policy.OnAccess(v) + return v.Value, v.TTL(), true } return nil, 0, false } -func resize(s *Store) { - bucket := make([]Node, 2*len(s.bucket)) +func (s *store) Get(key []byte) ([]byte, time.Duration, bool) { + s.mu.Lock() + defer s.mu.Unlock() + + return s.get(key) +} + +func resize(s *store) { + bucket := make([]node, 2*len(s.Bucket)) - for v := s.evict.EvictNext; v != &s.evict; v = v.EvictNext { + for v := s.Evict.EvictNext; v != &s.Evict; v = v.EvictNext { if !v.IsValid() { continue } @@ -114,52 +125,46 @@ func resize(s *Store) { v.HashPrev.HashNext = v } - s.bucket = bucket + s.Bucket = bucket } -func cleanup(s *Store) { - for v := s.evict.EvictNext; v != &s.evict; v = v.EvictNext { +func cleanup(s *store) { + for v := s.Evict.EvictNext; v != &s.Evict; v = v.EvictNext { if !v.IsValid() { deleteNode(s, v) } } } -func evict(s *Store) bool { - n := s.strategy.Evict() - if n == nil { - return false +func evict(s *store) bool { + for s.MaxCost != 0 && s.MaxCost < s.Cost { + n := s.Policy.Evict() + if n == nil { + break + } + deleteNode(s, n) } - deleteNode(s, n) return true } -func (s *Store) Set(key []byte, value []byte, ttl time.Duration) { - s.mu.Lock() - defer s.mu.Unlock() - - idx, hash := lookup(s, key) - bucket := &s.bucket[idx] - - lazyInitBucket(bucket) - - for v := bucket.HashNext; v != bucket; v = v.HashNext { - if bytes.Equal(key, v.Key) { - s.cost = s.cost + uint64(len(value)) - uint64(len(v.Value)) - v.Value = value - v.Expiration = time.Now().Add(ttl) - s.strategy.OnAccess(v) - } +func (s *store) set(key []byte, value []byte, ttl time.Duration) { + v, idx, hash := s.lookup(key) + if v != nil { + s.Cost = s.Cost + uint64(len(value)) - uint64(len(v.Value)) + v.Value = value + v.Expiration = time.Now().Add(ttl) + s.Policy.OnAccess(v) } - if float64(s.lenght)/float64(len(s.bucket)) > 0.75 { + bucket := &s.Bucket[idx] + if float64(s.Lenght)/float64(len(s.Bucket)) > 0.75 { resize(s) //resize may invidate pointer to bucket - bucket = &s.bucket[idx] + bucket = &s.Bucket[idx] lazyInitBucket(bucket) } - node := &Node{ + node := &node{ Hash: hash, Key: key, Value: value, @@ -174,14 +179,20 @@ func (s *Store) Set(key []byte, value []byte, ttl time.Duration) { node.HashNext.HashPrev = node node.HashPrev.HashNext = node - s.strategy.OnInsert(node) - s.strategy.OnAccess(node) + s.Policy.OnInsert(node) + + s.Cost = s.Cost + uint64(len(key)) + uint64(len(value)) + s.Lenght = s.Lenght + 1 +} + +func (s *store) Set(key []byte, value []byte, ttl time.Duration) { + s.mu.Lock() + defer s.mu.Unlock() - s.cost = s.cost + uint64(len(key)) + uint64(len(value)) - s.lenght = s.lenght + 1 + s.set(key, value, ttl) } -func deleteNode(s *Store, v *Node) { +func deleteNode(s *store, v *node) { v.HashNext.HashPrev = v.HashPrev v.HashPrev.HashNext = v.HashNext v.HashNext = nil @@ -192,30 +203,23 @@ func deleteNode(s *Store, v *Node) { v.EvictNext = nil v.EvictPrev = nil - s.cost = s.cost - (uint64(len(v.Key)) + uint64(len(v.Value))) - s.lenght = s.lenght - 1 + s.Cost = s.Cost - (uint64(len(v.Key)) + uint64(len(v.Value))) + s.Lenght = s.Lenght - 1 } -func (s *Store) Delete(key []byte) bool { - s.mu.Lock() - defer s.mu.Unlock() - - idx, _ := lookup(s, key) - - bucket := &s.bucket[idx] - - lazyInitBucket(bucket) - - for v := bucket.HashNext; v != bucket; v = v.HashNext { - if bytes.Equal(key, v.Key) { - deleteNode(s, v) - return true - } +func (s *store) delete(key []byte) bool { + v, _, _ := s.lookup(key) + if v != nil { + deleteNode(s, v) + return true } return false } -func (s *Store) Cost() uint64 { - return s.cost +func (s *store) Delete(key []byte) bool { + s.mu.Lock() + defer s.mu.Unlock() + + return s.delete(key) } diff --git a/map_test.go b/map_test.go index 0a758d0..328e03f 100644 --- a/map_test.go +++ b/map_test.go @@ -6,10 +6,10 @@ import ( "github.com/stretchr/testify/assert" ) -func setupTestStore(t testing.TB) *Store { +func setupTestStore(t testing.TB) *store { t.Helper() - store := &Store{} + store := &store{} store.Init() return store } @@ -22,6 +22,12 @@ func newPauseTimer(d time.Duration) *pauseTimer { return ret } +func newPauseTimerStopped(d time.Duration) *pauseTimer { + ret := newPauseTimer(d) + ret.Stop() + return ret +} + func (t *pauseTimer) Reset(d time.Duration) { t.duration = d if t.duration == 0 { |