diff options
-rw-r--r-- | conn.go | 39 | ||||
-rw-r--r-- | evict.go | 147 | ||||
-rw-r--r-- | evict_test.go | 213 | ||||
-rw-r--r-- | go.mod | 7 | ||||
-rw-r--r-- | go.sum | 43 | ||||
-rw-r--r-- | internal/pausedtimer/timer.go | 54 | ||||
-rw-r--r-- | internal/pausedtimer/timer_test.go (renamed from utils_test.go) | 12 | ||||
-rw-r--r-- | store.go (renamed from map.go) | 28 | ||||
-rw-r--r-- | store_test.go (renamed from map_test.go) | 45 | ||||
-rw-r--r-- | utils.go | 50 |
10 files changed, 432 insertions, 206 deletions
@@ -7,16 +7,15 @@ import ( "sync" "time" - "github.com/vmihailenco/msgpack" + "github.com/rogpeppe/go-internal/lockedfile" + "github.com/vmihailenco/msgpack/v5" ) type db struct { - File io.WriteSeeker - Store store - SnapshotTicker *pauseTimer - CleanupTicker *pauseTimer - Stop chan struct{} - wg sync.WaitGroup + File io.WriteSeeker + Store store + Stop chan struct{} + wg sync.WaitGroup } type Option func(*db) error @@ -26,7 +25,7 @@ func openFile(filename string, options ...Option) (*db, error) { if err != nil { return nil, err } - file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) + file, err := lockedfile.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return nil, err } @@ -49,10 +48,7 @@ func openFile(filename string, options ...Option) (*db, error) { } func openMem(options ...Option) (*db, error) { - ret := &db{ - SnapshotTicker: newPauseTimerStopped(0), - CleanupTicker: newPauseTimerStopped(10 * time.Second), - } + ret := &db{} ret.Store.Init() ret.SetConfig(options...) return ret, nil @@ -65,6 +61,9 @@ func (d *db) Start() { } func (d *db) SetConfig(options ...Option) error { + d.Store.mu.Lock() + defer d.Store.mu.Unlock() + for _, opt := range options { if err := opt(d); err != nil { return err @@ -88,14 +87,14 @@ func WithMaxCost(maxCost uint64) Option { func SetSnapshotTime(t time.Duration) Option { return func(d *db) error { - d.SnapshotTicker.Reset(t) + d.Store.SnapshotTicker.Reset(t) return nil } } func SetCleanupTime(t time.Duration) Option { return func(d *db) error { - d.CleanupTicker.Reset(t) + d.Store.CleanupTicker.Reset(t) return nil } } @@ -103,19 +102,19 @@ func SetCleanupTime(t time.Duration) Option { func (d *db) backgroundWorker() { defer d.wg.Done() - d.SnapshotTicker.Resume() - defer d.SnapshotTicker.Stop() + d.Store.SnapshotTicker.Resume() + defer d.Store.SnapshotTicker.Stop() - d.CleanupTicker.Resume() - defer d.CleanupTicker.Stop() + d.Store.CleanupTicker.Resume() + defer d.Store.CleanupTicker.Stop() for { select { case <-d.Stop: return - case <-d.SnapshotTicker.C: + case <-d.Store.SnapshotTicker.C: d.Flush() - case <-d.CleanupTicker.C: + case <-d.Store.CleanupTicker.C: cleanup(&d.Store) evict(&d.Store) } @@ -1,6 +1,8 @@ package cache -import "errors" +import ( + "errors" +) // EvictionPolicyType defines the type of eviction policy. type EvictionPolicyType int @@ -16,6 +18,7 @@ const ( // evictionStrategies interface defines the methods for eviction strategies. type evictionStrategies interface { OnInsert(n *node) + OnUpdate(n *node) OnAccess(n *node) Evict() *node } @@ -27,14 +30,21 @@ type evictionPolicy struct { evict *node } +func pushEvict(node *node, sentinnel *node) { + node.EvictPrev = sentinnel + node.EvictNext = node.EvictPrev.EvictNext + node.EvictNext.EvictPrev = node + node.EvictPrev.EvictNext = node +} + // SetPolicy sets the eviction policy based on the given type. func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error { store := map[EvictionPolicyType]func() evictionStrategies{ PolicyNone: func() evictionStrategies { - return nonePolicy{evict: e.evict} + return fifoPolicy{evict: e.evict, shouldEvict: false} }, PolicyFIFO: func() evictionStrategies { - return fifoPolicy{evict: e.evict} + return fifoPolicy{evict: e.evict, shouldEvict: true} }, PolicyLRU: func() evictionStrategies { return lruPolicy{evict: e.evict} @@ -42,6 +52,9 @@ func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error { PolicyLFU: func() evictionStrategies { return lfuPolicy{evict: e.evict} }, + PolicyLTR: func() evictionStrategies { + return ltrPolicy{evict: e.evict} + }, } factory, ok := store[y] if !ok { @@ -51,48 +64,28 @@ func (e *evictionPolicy) SetPolicy(y EvictionPolicyType) error { return nil } -// nonePolicy struct represents the no eviction policy. -type nonePolicy struct { - evict *node -} - -// OnInsert adds a node to the eviction list. -func (s nonePolicy) OnInsert(node *node) { - node.EvictPrev = s.evict - node.EvictNext = node.EvictPrev.EvictNext - node.EvictNext.EvictPrev = node - node.EvictPrev.EvictNext = node -} - -// OnAccess is a no-op for nonePolicy. -func (nonePolicy) OnAccess(n *node) { -} - -// Evict returns nil for nonePolicy. -func (nonePolicy) Evict() *node { - return nil -} - // fifoPolicy struct represents the First-In-First-Out eviction policy. type fifoPolicy struct { - evict *node + evict *node + shouldEvict bool } // OnInsert adds a node to the eviction list. func (s fifoPolicy) OnInsert(node *node) { - node.EvictPrev = s.evict - node.EvictNext = node.EvictPrev.EvictNext - node.EvictNext.EvictPrev = node - node.EvictPrev.EvictNext = node + pushEvict(node, s.evict) } // OnAccess is a no-op for fifoPolicy. func (fifoPolicy) OnAccess(n *node) { } +// OnUpdate is a no-op for fifoPolicy. +func (fifoPolicy) OnUpdate(n *node) { +} + // Evict returns the oldest node for fifoPolicy. func (s fifoPolicy) Evict() *node { - if s.evict.EvictPrev != s.evict { + if s.shouldEvict && s.evict.EvictPrev != s.evict { return s.evict.EvictPrev } else { return nil @@ -106,21 +99,18 @@ type lruPolicy struct { // OnInsert adds a node to the eviction list. func (s lruPolicy) OnInsert(node *node) { - node.EvictPrev = s.evict - node.EvictNext = node.EvictPrev.EvictNext - node.EvictNext.EvictPrev = node - node.EvictPrev.EvictNext = node + pushEvict(node, s.evict) +} + +func (s lruPolicy) OnUpdate(node *node) { + s.OnAccess(node) } // OnAccess moves the accessed node to the front of the eviction list. func (s lruPolicy) OnAccess(node *node) { node.EvictNext.EvictPrev = node.EvictPrev node.EvictPrev.EvictNext = node.EvictNext - - node.EvictPrev = s.evict - node.EvictNext = node.EvictPrev.EvictNext - node.EvictNext.EvictPrev = node - node.EvictPrev.EvictNext = node + s.OnInsert(node) } // Evict returns the least recently used node for lruPolicy. @@ -139,11 +129,12 @@ type lfuPolicy struct { // OnInsert adds a node to the eviction list and initializes its access count. 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 + pushEvict(node, s.evict) +} + +// OnUpdate increments the access count of the node and reorders the list. +func (s lfuPolicy) OnUpdate(node *node) { + s.OnAccess(node) } // OnAccess increments the access count of the node and reorders the list. @@ -179,3 +170,69 @@ func (s lfuPolicy) Evict() *node { return nil } } + +// ltrPolicy struct represents the Least Remaining Time eviction policy. +type ltrPolicy struct { + evict *node + evictZero bool +} + +// OnInsert adds a node to the eviction list based on its TTL (Time To Live). +// It places the node in the correct position in the list based on TTL. +func (s ltrPolicy) OnInsert(node *node) { + pushEvict(node, s.evict) + + s.OnUpdate(node) +} + +// OnAccess is a no-op for ltrPolicy. +// It does not perform any action when a node is accessed. +func (s ltrPolicy) OnAccess(node *node) { +} + +// OnUpdate updates the position of the node in the eviction list based on its TTL. +// It reorders the list to maintain the correct order based on TTL. +func (s ltrPolicy) OnUpdate(node *node) { + if node.TTL() == 0 { + return + } + for v := node.EvictPrev; v.EvictPrev != s.evict; v = v.EvictPrev { + if v.TTL() == 0 { + continue + } + if v.TTL() < node.TTL() { + node.EvictNext.EvictPrev = node.EvictPrev + node.EvictPrev.EvictNext = node.EvictNext + + node.EvictPrev = v + node.EvictNext = node.EvictPrev.EvictNext + node.EvictNext.EvictPrev = node + node.EvictPrev.EvictNext = node + return + } + } + for v := node.EvictNext; v.EvictNext != s.evict; v = v.EvictNext { + if v.TTL() == 0 { + continue + } + if v.TTL() > node.TTL() { + node.EvictNext.EvictPrev = node.EvictPrev + node.EvictPrev.EvictNext = node.EvictNext + + node.EvictPrev = v + node.EvictNext = node.EvictPrev.EvictNext + node.EvictNext.EvictPrev = node + node.EvictPrev.EvictNext = node + return + } + } +} + +// Evict returns the node with the least remaining time to live for ltrPolicy. +// It returns the node at the end of the eviction list. +func (s ltrPolicy) Evict() *node { + if s.evict.EvictPrev != s.evict && (s.evict.EvictPrev.TTL() != 0 || s.evictZero) { + return s.evict.EvictPrev + } + return nil +} diff --git a/evict_test.go b/evict_test.go index 3503be4..ba73c24 100644 --- a/evict_test.go +++ b/evict_test.go @@ -2,6 +2,7 @@ package cache import ( "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -29,44 +30,13 @@ func getListOrder(t testing.TB, evict *node) []*node { 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)} + policy := fifoPolicy{evict: createSentinel(t), shouldEvict: true} n0 := &node{Key: []byte("0")} n1 := &node{Key: []byte("1")} @@ -86,7 +56,7 @@ func TestFIFOPolicy(t *testing.T) { t.Run("Evict", func(t *testing.T) { t.Parallel() - policy := fifoPolicy{evict: createSentinel(t)} + policy := fifoPolicy{evict: createSentinel(t), shouldEvict: true} n0 := &node{Key: []byte("0")} n1 := &node{Key: []byte("1")} @@ -97,6 +67,17 @@ func TestFIFOPolicy(t *testing.T) { evictedNode := policy.Evict() assert.Same(t, n0, evictedNode) }) + + t.Run("Evict noEvict", func(t *testing.T) { + t.Parallel() + + policy := fifoPolicy{evict: createSentinel(t), shouldEvict: false} + + policy.OnInsert(&node{}) + + assert.Nil(t, policy.Evict()) + }) + t.Run("Empty List", func(t *testing.T) { t.Parallel() @@ -269,7 +250,7 @@ func TestLFUPolicy(t *testing.T) { policy := lfuPolicy{evict: createSentinel(t)} - n0 := &node{Key: []byte("1")} + n0 := &node{Key: []byte("0")} n1 := &node{Key: []byte("1")} policy.OnInsert(n0) @@ -293,3 +274,167 @@ func TestLFUPolicy(t *testing.T) { }) }) } + +func TestLTRPolicy(t *testing.T) { + t.Parallel() + + t.Run("OnInsert", func(t *testing.T) { + t.Parallel() + + t.Run("With TTL", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, n0, order[0]) + assert.Same(t, n1, order[1]) + }) + + t.Run("Without TTL", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + 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, n1, order[0]) + assert.Same(t, n0, order[1]) + }) + }) + + t.Run("OnUpdate", func(t *testing.T) { + t.Parallel() + + t.Run("With TTL", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + n0.Expiration = time.Now().Add(3 * time.Hour) + policy.OnUpdate(n0) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, n0, order[1]) + assert.Same(t, n1, order[0]) + }) + + t.Run("With TTL Decrease", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + n1.Expiration = time.Now().Add(30 * time.Minute) + policy.OnUpdate(n1) + + order := getListOrder(t, policy.evict) + assert.Len(t, order, 2) + assert.Same(t, n1, order[1]) + assert.Same(t, n0, order[0]) + }) + }) + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + t.Run("Evict", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + 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("Evict TTL", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + evictedNode := policy.Evict() + assert.Same(t, n1, evictedNode) + }) + + t.Run("Evict TTL Update", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + n0.Expiration = time.Now().Add(3 * time.Hour) + policy.OnUpdate(n0) + + evictedNode := policy.Evict() + assert.Same(t, n0, evictedNode) + }) + + t.Run("Evict TTL Update Down", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + n0 := &node{Key: []byte("0"), Expiration: time.Now().Add(1 * time.Hour)} + n1 := &node{Key: []byte("1"), Expiration: time.Now().Add(2 * time.Hour)} + + policy.OnInsert(n0) + policy.OnInsert(n1) + + n1.Expiration = time.Now().Add(20 * time.Minute) + policy.OnUpdate(n1) + + evictedNode := policy.Evict() + assert.Same(t, n1, evictedNode) + }) + + t.Run("Empty List", func(t *testing.T) { + t.Parallel() + + policy := ltrPolicy{evict: createSentinel(t), evictZero: true} + + assert.Nil(t, policy.Evict()) + }) + }) +} @@ -3,15 +3,14 @@ module github.com/marcthe12/cache go 1.24.0 require ( + github.com/rogpeppe/go-internal v1.13.1 github.com/stretchr/testify v1.10.0 - github.com/vmihailenco/msgpack v4.0.4+incompatible + github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.26.0 // indirect ) @@ -1,45 +1,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= -github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/pausedtimer/timer.go b/internal/pausedtimer/timer.go new file mode 100644 index 0000000..691c257 --- /dev/null +++ b/internal/pausedtimer/timer.go @@ -0,0 +1,54 @@ +package pausedtimer + +import ( + "math" + "time" +) + +// PauseTimer is a struct that wraps a time.Ticker and provides additional functionality +// to pause and resume the ticker. +// If the duration is 0, the timer is created in a stopped state. +type PauseTimer struct { + *time.Ticker + duration time.Duration +} + +// New creates a new pauseTimer with the specified duration. +func New(d time.Duration) *PauseTimer { + ret := &PauseTimer{duration: d} + if d != 0 { + ret.Ticker = time.NewTicker(d) + } else { + ret.Ticker = time.NewTicker(math.MaxInt64) + ret.Reset(0) + } + return ret +} + +// NewStopped creates a new pauseTimer with the specified duration and stops it immediately. +func NewStopped(d time.Duration) *PauseTimer { + ret := New(d) + ret.Stop() + return ret +} + +// Reset sets the timer to the specified duration and starts it. +// If the duration is 0, the timer is stopped. +func (t *PauseTimer) Reset(d time.Duration) { + t.duration = d + if t.duration == 0 { + t.Stop() + } else { + t.Ticker.Reset(d) + } +} + +// Resume resumes the timer with its last set duration. +func (t *PauseTimer) Resume() { + t.Reset(t.GetDuration()) +} + +// GetDuration returns the current duration of the timer. +func (t *PauseTimer) GetDuration() time.Duration { + return t.duration +} diff --git a/utils_test.go b/internal/pausedtimer/timer_test.go index 329b166..6e15001 100644 --- a/utils_test.go +++ b/internal/pausedtimer/timer_test.go @@ -1,4 +1,4 @@ -package cache +package pausedtimer import ( "testing" @@ -7,16 +7,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewPauseTimer(t *testing.T) { +func TestNew(t *testing.T) { d := 1 * time.Second - timer := newPauseTimer(d) + timer := New(d) assert.Equal(t, d, timer.duration) assert.NotNil(t, timer.Ticker) } func TestPauseTimerReset(t *testing.T) { d := 1 * time.Second - timer := newPauseTimer(d) + timer := New(d) newD := 2 * time.Second timer.Reset(newD) assert.Equal(t, newD, timer.duration) @@ -24,13 +24,13 @@ func TestPauseTimerReset(t *testing.T) { func TestPauseTimerResume(t *testing.T) { d := 1 * time.Second - timer := newPauseTimerStopped(d) + timer := NewStopped(d) timer.Resume() assert.Equal(t, d, timer.duration) } func TestPauseTimerGetDuration(t *testing.T) { d := 1 * time.Second - timer := newPauseTimer(d) + timer := New(d) assert.Equal(t, d, timer.GetDuration()) } @@ -4,8 +4,12 @@ import ( "bytes" "sync" "time" + + "github.com/marcthe12/cache/internal/pausedtimer" ) +const initialBucketSize uint64 = 8 + type node struct { Hash uint64 Expiration time.Time @@ -31,18 +35,22 @@ func (n *node) TTL() time.Duration { } type store struct { - Bucket []node - Length uint64 - Cost uint64 - Evict node - MaxCost uint64 - Policy evictionPolicy - mu sync.Mutex + Bucket []node + Length uint64 + Cost uint64 + Evict node + MaxCost uint64 + SnapshotTicker *pausedtimer.PauseTimer + CleanupTicker *pausedtimer.PauseTimer + Policy evictionPolicy + mu sync.Mutex } func (s *store) Init() { s.Clear() s.Policy.evict = &s.Evict + s.SnapshotTicker = pausedtimer.NewStopped(0) + s.CleanupTicker = pausedtimer.NewStopped(10 * time.Second) s.Policy.SetPolicy(PolicyNone) } @@ -50,7 +58,7 @@ func (s *store) Clear() { s.mu.Lock() defer s.mu.Unlock() - s.Bucket = make([]node, 8) + s.Bucket = make([]node, initialBucketSize) s.Length = 0 s.Cost = 0 @@ -112,6 +120,7 @@ func resize(s *store) { for v := s.Evict.EvictNext; v != &s.Evict; v = v.EvictNext { if !v.IsValid() { + deleteNode(s, v) continue } idx := v.Hash % uint64(len(bucket)) @@ -153,13 +162,14 @@ func (s *store) set(key []byte, value []byte, ttl time.Duration) { s.Cost = s.Cost + uint64(len(value)) - uint64(len(v.Value)) v.Value = value v.Expiration = time.Now().Add(ttl) - s.Policy.OnAccess(v) + s.Policy.OnUpdate(v) } bucket := &s.Bucket[idx] if float64(s.Length)/float64(len(s.Bucket)) > 0.75 { resize(s) //resize may invidate pointer to bucket + _, idx, _ := s.lookup(key) bucket = &s.Bucket[idx] lazyInitBucket(bucket) } diff --git a/map_test.go b/store_test.go index 5610d0a..3ad33e5 100644 --- a/map_test.go +++ b/store_test.go @@ -1,7 +1,9 @@ package cache import ( + "encoding/binary" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -23,12 +25,26 @@ func TestStoreGetSet(t *testing.T) { store := setupTestStore(t) want := []byte("Value") - store.Set([]byte("Key"), want, 0) - got, _, ok := store.Get([]byte("Key")) + store.Set([]byte("Key"), want, 1*time.Hour) + got, ttl, ok := store.Get([]byte("Key")) assert.Equal(t, want, got) + + now := time.Now() + assert.WithinDuration(t, now.Add(ttl), now.Add(1*time.Hour), 1*time.Millisecond) assert.True(t, ok) }) + t.Run("Exists TTL", func(t *testing.T) { + t.Parallel() + + store := setupTestStore(t) + + want := []byte("Value") + store.Set([]byte("Key"), want, time.Nanosecond) + _, _, ok := store.Get([]byte("Key")) + assert.False(t, ok) + }) + t.Run("Not Exists", func(t *testing.T) { t.Parallel() @@ -50,6 +66,31 @@ func TestStoreGetSet(t *testing.T) { assert.Equal(t, want, got) assert.True(t, ok) }) + + t.Run("Resize", func(t *testing.T) { + t.Parallel() + + store := setupTestStore(t) + + for i := range initialBucketSize { + key := binary.LittleEndian.AppendUint64(nil, i) + store.Set(key, key, 0) + } + + for i := range store.Length { + key := binary.LittleEndian.AppendUint64(nil, i) + _, _, ok := store.Get(key) + assert.True(t, ok, i) + } + + assert.Len(t, store.Bucket, int(initialBucketSize)*2) + + for i := range store.Length { + key := binary.LittleEndian.AppendUint64(nil, i) + _, _, ok := store.Get(key) + assert.True(t, ok, i) + } + }) } func TestStoreDelete(t *testing.T) { @@ -2,58 +2,8 @@ package cache import ( "hash/fnv" - "math" - "time" ) -// pauseTimer is a struct that wraps a time.Ticker and provides additional functionality -// to pause and resume the ticker. -// If the duration is 0, the timer is created in a stopped state. -type pauseTimer struct { - *time.Ticker - duration time.Duration -} - -// newPauseTimer creates a new pauseTimer with the specified duration. -func newPauseTimer(d time.Duration) *pauseTimer { - ret := &pauseTimer{duration: d} - if d != 0 { - ret.Ticker = time.NewTicker(d) - } else { - ret.Ticker = time.NewTicker(math.MaxInt64) - ret.Reset(0) - } - return ret -} - -// newPauseTimerStopped creates a new pauseTimer with the specified duration and stops it immediately. -func newPauseTimerStopped(d time.Duration) *pauseTimer { - ret := newPauseTimer(d) - ret.Stop() - return ret -} - -// Reset sets the timer to the specified duration and starts it. -// If the duration is 0, the timer is stopped. -func (t *pauseTimer) Reset(d time.Duration) { - t.duration = d - if t.duration == 0 { - t.Stop() - } else { - t.Ticker.Reset(d) - } -} - -// Resume resumes the timer with its last set duration. -func (t *pauseTimer) Resume() { - t.Reset(t.GetDuration()) -} - -// GetDuration returns the current duration of the timer. -func (t *pauseTimer) GetDuration() time.Duration { - return t.duration -} - // zero returns the zero value for the specified type. func zero[T any]() T { var ret T |