mirror of
https://github.com/1f349/cache.git
synced 2025-04-16 08:17:49 +01:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
267e895bd8 | |||
6839f0fc0f | |||
76230a5bfc | |||
1026af286f | |||
708640f8a0 | |||
cb931c3fa6 | |||
7147708296 | |||
112b816e53 | |||
7e244a930f | |||
fdd568801d |
14
.github/workflows/test.yml
vendored
Normal file
14
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
on: [push, pull_request]
|
||||||
|
name: Test
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.23.x]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: go test ./...
|
72
cache.go
72
cache.go
@ -1,7 +1,6 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/MrMelon54/rescheduler"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -19,7 +18,6 @@ func timeUntil(t time.Time) time.Duration { return t.Sub(timeNow()) }
|
|||||||
type Cache[K comparable, V any] struct {
|
type Cache[K comparable, V any] struct {
|
||||||
items sync.Map
|
items sync.Map
|
||||||
chain *keyed[K] // linked list of to-expire keys
|
chain *keyed[K] // linked list of to-expire keys
|
||||||
sched *rescheduler.Rescheduler
|
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
chainAdd chan keyed[K]
|
chainAdd chan keyed[K]
|
||||||
chainDel chan K
|
chainDel chan K
|
||||||
@ -54,7 +52,7 @@ func New[K comparable, V any]() *Cache[K, V] {
|
|||||||
chainAdd: make(chan keyed[K], 1),
|
chainAdd: make(chan keyed[K], 1),
|
||||||
chainDel: make(chan K, 1),
|
chainDel: make(chan K, 1),
|
||||||
}
|
}
|
||||||
c.sched = rescheduler.NewRescheduler(c.cleaner)
|
go c.cleaner()
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,70 +66,33 @@ func (c *Cache[K, V]) Close() {
|
|||||||
// cleaner handles removing expired keys. The chainAdd and chainDel channels are
|
// cleaner handles removing expired keys. The chainAdd and chainDel channels are
|
||||||
// handled here to prevent race conditions. This ensures the expiry timer can be
|
// handled here to prevent race conditions. This ensures the expiry timer can be
|
||||||
// stopped before modifying the chain.
|
// stopped before modifying the chain.
|
||||||
//
|
|
||||||
// The cleaner is stopped whenever the chain is empty due to there being no chain
|
|
||||||
// to manage.
|
|
||||||
func (c *Cache[K, V]) cleaner() {
|
func (c *Cache[K, V]) cleaner() {
|
||||||
// cleaner is always called from Set or Delete methods with a value sent on chainAdd or chainDel
|
|
||||||
select {
|
|
||||||
case node := <-c.chainAdd:
|
|
||||||
c.chainInsert(node)
|
|
||||||
case key := <-c.chainDel:
|
|
||||||
c.chainSplice(key)
|
|
||||||
default:
|
|
||||||
// skip if chainAdd or chainDel isn't ready
|
|
||||||
|
|
||||||
println("Skip first select")
|
|
||||||
}
|
|
||||||
|
|
||||||
// at this point if the chain is empty then exit
|
|
||||||
if c.chain == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a timer for the next expiry
|
|
||||||
t := time.NewTimer(timeUntil(c.chain.expires))
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-c.close:
|
case <-c.close:
|
||||||
// exit the cleaner goroutine
|
// exit the cleaner goroutine
|
||||||
return
|
return
|
||||||
case node := <-c.chainAdd:
|
case node := <-c.chainAdd:
|
||||||
// stop the timer safely
|
|
||||||
if !t.Stop() {
|
|
||||||
<-t.C
|
|
||||||
}
|
|
||||||
// the chain will not be empty after this insert so no check is required
|
|
||||||
c.chainInsert(node)
|
c.chainInsert(node)
|
||||||
case key := <-c.chainDel:
|
case key := <-c.chainDel:
|
||||||
// stop the timer safely
|
|
||||||
if !t.Stop() {
|
|
||||||
<-t.C
|
|
||||||
}
|
|
||||||
c.chainSplice(key)
|
c.chainSplice(key)
|
||||||
case <-t.C:
|
case <-c.nextExpiry():
|
||||||
// if there is no chain then kill the expiry scheduler
|
|
||||||
if c.chain == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all expired entries
|
// remove all expired entries
|
||||||
for c.chain != nil && c.chain.HasExpired() {
|
for c.chain != nil && c.chain.HasExpired() {
|
||||||
c.items.Delete(c.chain.data)
|
c.items.CompareAndDelete(c.chain.data, c.chain.item)
|
||||||
c.chain = c.chain.next
|
c.chain = c.chain.next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is no chain then kill the expiry scheduler
|
|
||||||
if c.chain == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Reset(timeUntil(c.chain.expires))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache[K, V]) nextExpiry() <-chan time.Time {
|
||||||
|
if c.chain == nil {
|
||||||
|
return make(chan time.Time)
|
||||||
|
}
|
||||||
|
return time.After(timeUntil(c.chain.expires))
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache[K, V]) chainInsert(node keyed[K]) {
|
func (c *Cache[K, V]) chainInsert(node keyed[K]) {
|
||||||
// quick path for an empty chain
|
// quick path for an empty chain
|
||||||
if c.chain == nil {
|
if c.chain == nil {
|
||||||
@ -232,10 +193,17 @@ func (c *Cache[K, V]) SetPermanent(key K, value V) {
|
|||||||
c.items.Store(key, i)
|
c.items.Store(key, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set adds an item to the cache with an expiry date.
|
// Set adds an item to the cache with an expiry duration.
|
||||||
//
|
//
|
||||||
// If an item is added with the same key then this item with be overwritten.
|
// If an item is added with the same key then this item with be overwritten.
|
||||||
func (c *Cache[K, V]) Set(key K, value V, expires time.Time) {
|
func (c *Cache[K, V]) Set(key K, value V, expires time.Duration) {
|
||||||
|
c.SetAbs(key, value, timeNow().Add(expires))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAbs adds an item to the cache with an expiry date.
|
||||||
|
//
|
||||||
|
// If an item is added with the same key then this item with be overwritten.
|
||||||
|
func (c *Cache[K, V]) SetAbs(key K, value V, expires time.Time) {
|
||||||
// if the cache is closed then just return
|
// if the cache is closed then just return
|
||||||
select {
|
select {
|
||||||
case <-c.close:
|
case <-c.close:
|
||||||
@ -250,7 +218,6 @@ func (c *Cache[K, V]) Set(key K, value V, expires time.Time) {
|
|||||||
i := &item[V]{data: value, expires: expires}
|
i := &item[V]{data: value, expires: expires}
|
||||||
c.items.Store(key, i)
|
c.items.Store(key, i)
|
||||||
c.chainAdd <- keyed[K]{item: item[K]{data: key, expires: expires}}
|
c.chainAdd <- keyed[K]{item: item[K]{data: key, expires: expires}}
|
||||||
c.sched.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes an item from the cache.
|
// Delete removes an item from the cache.
|
||||||
@ -264,7 +231,6 @@ func (c *Cache[K, V]) Delete(key K) {
|
|||||||
|
|
||||||
c.items.Delete(key)
|
c.items.Delete(key)
|
||||||
c.chainDel <- key
|
c.chainDel <- key
|
||||||
c.sched.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range calls f with every key-value pair, which has not expired, currently
|
// Range calls f with every key-value pair, which has not expired, currently
|
||||||
|
@ -71,8 +71,26 @@ func TestCache_SetPermanent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_Set(t *testing.T) {
|
func TestCache_Set(t *testing.T) {
|
||||||
|
timeNow = func() time.Time { return time.Date(nextYear, time.January, 1, 0, 0, 0, 0, time.UTC) }
|
||||||
|
|
||||||
c := New[string, string]()
|
c := New[string, string]()
|
||||||
c.Set("a", "b", time.Date(nextYear, time.January, 1, 0, 0, 0, 0, time.UTC))
|
c.Set("a", "b", 5*time.Minute)
|
||||||
|
|
||||||
|
value, ok := c.items.Load("a")
|
||||||
|
v := value.(*item[string])
|
||||||
|
assert.Equal(t, item[string]{
|
||||||
|
data: "b",
|
||||||
|
expires: timeNow().Add(5 * time.Minute),
|
||||||
|
}, *v)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
value, ok = c.items.Load("b")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_SetAbs(t *testing.T) {
|
||||||
|
c := New[string, string]()
|
||||||
|
c.SetAbs("a", "b", time.Date(nextYear, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
value, ok := c.items.Load("a")
|
value, ok := c.items.Load("a")
|
||||||
v := value.(*item[string])
|
v := value.(*item[string])
|
||||||
@ -93,7 +111,7 @@ func TestCache_Delete(t *testing.T) {
|
|||||||
c.Delete("a")
|
c.Delete("a")
|
||||||
|
|
||||||
// scheduler should finish after deleting the item
|
// scheduler should finish after deleting the item
|
||||||
c.sched.Wait()
|
time.Sleep(time.Second)
|
||||||
assert.Nil(t, c.chain)
|
assert.Nil(t, c.chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,11 +128,10 @@ func TestCache_Range(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_Cleaner(t *testing.T) {
|
func TestCache_Cleaner(t *testing.T) {
|
||||||
timeNow = func() time.Time { return time.Now() }
|
timeNow = time.Now
|
||||||
|
|
||||||
n := time.Now().Add(2 * time.Second)
|
|
||||||
c := New[string, string]()
|
c := New[string, string]()
|
||||||
c.Set("a", "b", n)
|
c.Set("a", "b", 2*time.Second)
|
||||||
|
|
||||||
// check before expiry
|
// check before expiry
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@ -129,6 +146,44 @@ func TestCache_Cleaner(t *testing.T) {
|
|||||||
assert.Equal(t, "", get)
|
assert.Equal(t, "", get)
|
||||||
|
|
||||||
// scheduler should finish after the chain is empty
|
// scheduler should finish after the chain is empty
|
||||||
c.sched.Wait()
|
time.Sleep(time.Second)
|
||||||
assert.Nil(t, c.chain)
|
assert.Nil(t, c.chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCache_UpdateExpiry(t *testing.T) {
|
||||||
|
timeNow = time.Now
|
||||||
|
|
||||||
|
c := New[string, string]()
|
||||||
|
c.Set("a", "b", 2*time.Second)
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
get, b := c.Get("a")
|
||||||
|
assert.True(t, b)
|
||||||
|
assert.Equal(t, "b", get)
|
||||||
|
|
||||||
|
c.Set("a", "b", 5*time.Second)
|
||||||
|
|
||||||
|
// after expiry of the first set call
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
get, b = c.Get("a")
|
||||||
|
assert.True(t, b)
|
||||||
|
assert.Equal(t, "b", get)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_ClearerDeath(t *testing.T) {
|
||||||
|
timeNow = time.Now
|
||||||
|
|
||||||
|
c := New[string, string]()
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
var added bool
|
||||||
|
go func() {
|
||||||
|
c.chainAdd <- keyed[string]{item: item[string]{data: "a"}}
|
||||||
|
c.chainAdd <- keyed[string]{item: item[string]{data: "b"}}
|
||||||
|
added = true
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
assert.True(t, added)
|
||||||
|
}
|
||||||
|
7
go.mod
7
go.mod
@ -1,11 +1,8 @@
|
|||||||
module github.com/1f349/cache
|
module github.com/1f349/cache
|
||||||
|
|
||||||
go 1.21.1
|
go 1.23
|
||||||
|
|
||||||
require (
|
require github.com/stretchr/testify v1.10.0
|
||||||
github.com/MrMelon54/rescheduler v0.0.2
|
|
||||||
github.com/stretchr/testify v1.8.4
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -1,11 +1,9 @@
|
|||||||
github.com/MrMelon54/rescheduler v0.0.2 h1:efrRwr0BYlkaXFucZDjQqRyIawZiMEAnzjea46Bs9Oc=
|
|
||||||
github.com/MrMelon54/rescheduler v0.0.2/go.mod h1:OQDFtZHdS4/qA/r7rtJUQA22/hbpnZ9MGQCXOPjhC6w=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
Loading…
x
Reference in New Issue
Block a user