mirror of
https://github.com/1f349/cache.git
synced 2025-04-16 00:07:57 +01:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
267e895bd8 | |||
6839f0fc0f | |||
76230a5bfc |
62
cache.go
62
cache.go
@ -66,68 +66,33 @@ func (c *Cache[K, V]) Close() {
|
||||
// cleaner handles removing expired keys. The chainAdd and chainDel channels are
|
||||
// handled here to prevent race conditions. This ensures the expiry timer can be
|
||||
// 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() {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
select {
|
||||
case <-c.close:
|
||||
// exit the cleaner goroutine
|
||||
return
|
||||
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)
|
||||
case key := <-c.chainDel:
|
||||
// stop the timer safely
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
c.chainSplice(key)
|
||||
case <-t.C:
|
||||
// if there is no chain then kill the expiry scheduler
|
||||
if c.chain == nil {
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.nextExpiry():
|
||||
// remove all expired entries
|
||||
for c.chain != nil && c.chain.HasExpired() {
|
||||
c.items.CompareAndDelete(c.chain.data, c.chain.item)
|
||||
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]) {
|
||||
// quick path for an empty chain
|
||||
if c.chain == nil {
|
||||
@ -228,10 +193,17 @@ func (c *Cache[K, V]) SetPermanent(key K, value V) {
|
||||
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.
|
||||
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
|
||||
select {
|
||||
case <-c.close:
|
||||
|
@ -71,8 +71,26 @@ func TestCache_SetPermanent(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.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")
|
||||
v := value.(*item[string])
|
||||
@ -110,11 +128,10 @@ func TestCache_Range(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.Set("a", "b", n)
|
||||
c.Set("a", "b", 2*time.Second)
|
||||
|
||||
// check before expiry
|
||||
time.Sleep(time.Second)
|
||||
@ -134,17 +151,17 @@ func TestCache_Cleaner(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCache_UpdateExpiry(t *testing.T) {
|
||||
timeNow = func() time.Time { return time.Now() }
|
||||
timeNow = time.Now
|
||||
|
||||
c := New[string, string]()
|
||||
c.Set("a", "b", time.Now().Add(2*time.Second))
|
||||
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", time.Now().Add(5*time.Second))
|
||||
c.Set("a", "b", 5*time.Second)
|
||||
|
||||
// after expiry of the first set call
|
||||
time.Sleep(2 * time.Second)
|
||||
@ -152,3 +169,21 @@ func TestCache_UpdateExpiry(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user