fix expiration for non by key cache

add goroutine to clean up old cache entries to save mem
This commit is contained in:
maddalax 2024-09-28 12:38:42 -05:00
parent beeb8bbe30
commit af81f53160
2 changed files with 212 additions and 16 deletions

View file

@ -1,6 +1,7 @@
package h package h
import ( import (
"log/slog"
"sync" "sync"
"time" "time"
) )
@ -11,8 +12,8 @@ type CachedNode struct {
byKeyCache map[any]*Entry byKeyCache map[any]*Entry
byKeyExpiration map[any]time.Time byKeyExpiration map[any]time.Time
mutex sync.Mutex mutex sync.Mutex
expiration time.Time
duration time.Duration duration time.Duration
expiration time.Time
html string html string
} }
@ -33,15 +34,25 @@ type GetElementFuncT2WithKey[K comparable, T any, T2 any] func(T, T2) (K, GetEle
type GetElementFuncT3WithKey[K comparable, T any, T2 any, T3 any] func(T, T2, T3) (K, GetElementFunc) type GetElementFuncT3WithKey[K comparable, T any, T2 any, T3 any] func(T, T2, T3) (K, GetElementFunc)
type GetElementFuncT4WithKey[K comparable, T any, T2 any, T3 any, T4 any] func(T, T2, T3, T4) (K, GetElementFunc) type GetElementFuncT4WithKey[K comparable, T any, T2 any, T3 any, T4 any] func(T, T2, T3, T4) (K, GetElementFunc)
func startExpiredCacheCleaner(node *CachedNode) {
go func() {
for {
time.Sleep(time.Second)
node.ClearExpired()
}
}()
}
func Cached(duration time.Duration, cb GetElementFunc) func() *Element { func Cached(duration time.Duration, cb GetElementFunc) func() *Element {
element := &Element{ element := &Element{
tag: CachedNodeTag, tag: CachedNodeTag,
meta: &CachedNode{ meta: &CachedNode{
cb: cb, cb: cb,
html: "", html: "",
expiration: time.Now().Add(duration), duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func() *Element { return func() *Element {
return element return element
} }
@ -57,6 +68,7 @@ func CachedPerKey[K comparable](duration time.Duration, cb GetElementFuncWithKey
duration: duration, duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func() *Element { return func() *Element {
key, componentFunc := cb() key, componentFunc := cb()
return &Element{ return &Element{
@ -86,6 +98,7 @@ func CachedPerKeyT[K comparable, T any](duration time.Duration, cb GetElementFun
duration: duration, duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T) *Element { return func(data T) *Element {
key, componentFunc := cb(data) key, componentFunc := cb(data)
return &Element{ return &Element{
@ -109,6 +122,7 @@ func CachedPerKeyT2[K comparable, T any, T2 any](duration time.Duration, cb GetE
duration: duration, duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2) *Element { return func(data T, data2 T2) *Element {
key, componentFunc := cb(data, data2) key, componentFunc := cb(data, data2)
return &Element{ return &Element{
@ -132,6 +146,7 @@ func CachedPerKeyT3[K comparable, T any, T2 any, T3 any](duration time.Duration,
duration: duration, duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3) *Element { return func(data T, data2 T2, data3 T3) *Element {
key, componentFunc := cb(data, data2, data3) key, componentFunc := cb(data, data2, data3)
return &Element{ return &Element{
@ -155,6 +170,7 @@ func CachedPerKeyT4[K comparable, T any, T2 any, T3 any, T4 any](duration time.D
duration: duration, duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3, data4 T4) *Element { return func(data T, data2 T2, data3 T3, data4 T4) *Element {
key, componentFunc := cb(data, data2, data3, data4) key, componentFunc := cb(data, data2, data3, data4)
return &Element{ return &Element{
@ -172,11 +188,12 @@ func CachedT[T any](duration time.Duration, cb GetElementFuncT[T]) func(T) *Elem
element := &Element{ element := &Element{
tag: CachedNodeTag, tag: CachedNodeTag,
meta: &CachedNode{ meta: &CachedNode{
html: "", html: "",
expiration: time.Now().Add(duration), duration: duration,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T) *Element { return func(data T) *Element {
element.meta.(*CachedNode).cb = func() *Element { element.meta.(*CachedNode).cb = func() *Element {
return cb(data) return cb(data)
@ -189,10 +206,11 @@ func CachedT2[T any, T2 any](duration time.Duration, cb GetElementFuncT2[T, T2])
element := &Element{ element := &Element{
tag: CachedNodeTag, tag: CachedNodeTag,
meta: &CachedNode{ meta: &CachedNode{
html: "", html: "",
expiration: time.Now().Add(duration), duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2) *Element { return func(data T, data2 T2) *Element {
element.meta.(*CachedNode).cb = func() *Element { element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2) return cb(data, data2)
@ -205,10 +223,11 @@ func CachedT3[T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3
element := &Element{ element := &Element{
tag: CachedNodeTag, tag: CachedNodeTag,
meta: &CachedNode{ meta: &CachedNode{
html: "", html: "",
expiration: time.Now().Add(duration), duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3) *Element { return func(data T, data2 T2, data3 T3) *Element {
element.meta.(*CachedNode).cb = func() *Element { element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2, data3) return cb(data, data2, data3)
@ -221,10 +240,11 @@ func CachedT4[T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetEleme
element := &Element{ element := &Element{
tag: CachedNodeTag, tag: CachedNodeTag,
meta: &CachedNode{ meta: &CachedNode{
html: "", html: "",
expiration: time.Now().Add(duration), duration: duration,
}, },
} }
startExpiredCacheCleaner(element.meta.(*CachedNode))
return func(data T, data2 T2, data3 T3, data4 T4) *Element { return func(data T, data2 T2, data3 T3, data4 T4) *Element {
element.meta.(*CachedNode).cb = func() *Element { element.meta.(*CachedNode).cb = func() *Element {
return cb(data, data2, data3, data4) return cb(data, data2, data3, data4)
@ -240,6 +260,40 @@ func (c *CachedNode) ClearCache() {
delete(c.byKeyCache, key) delete(c.byKeyCache, key)
} }
} }
if c.byKeyExpiration != nil {
for key := range c.byKeyExpiration {
delete(c.byKeyExpiration, key)
}
}
}
func (c *CachedNode) ClearExpired() {
c.mutex.Lock()
defer c.mutex.Unlock()
deletedCount := 0
if c.isByKey == true {
if c.byKeyCache != nil && c.byKeyExpiration != nil {
for key := range c.byKeyCache {
expir, ok := c.byKeyExpiration[key]
if ok && expir.Before(time.Now()) {
delete(c.byKeyCache, key)
delete(c.byKeyExpiration, key)
deletedCount++
}
}
}
} else {
now := time.Now()
expiration := c.expiration
if c.html != "" && expiration.Before(now) {
c.html = ""
deletedCount++
}
}
if deletedCount > 0 {
slog.Debug("Deleted expired cache entries", slog.Int("count", deletedCount))
}
} }
func (c *CachedNode) Render(ctx *RenderContext) { func (c *CachedNode) Render(ctx *RenderContext) {
@ -248,8 +302,13 @@ func (c *CachedNode) Render(ctx *RenderContext) {
} else { } else {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if c.expiration.Before(time.Now()) {
now := time.Now()
expiration := c.expiration
if expiration.IsZero() || expiration.Before(now) {
c.html = "" c.html = ""
c.expiration = now.Add(c.duration)
} }
if c.html != "" { if c.html != "" {
@ -308,5 +367,4 @@ func (c *ByKeyEntry) Render(ctx *RenderContext) {
// exists in cache and not expired // exists in cache and not expired
ctx.builder.WriteString(entry.html) ctx.builder.WriteString(entry.html)
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/html" "golang.org/x/net/html"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -141,6 +142,70 @@ func TestCached(t *testing.T) {
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
} }
func TestCachedT(t *testing.T) {
t.Parallel()
count := 0
page := CachedT(time.Hour, func(a string) *Element {
count++
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a")))
secondRender := sortHtmlAttributes(Render(page("a")))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
}
func TestCachedT2(t *testing.T) {
t.Parallel()
count := 0
page := CachedT2(time.Hour, func(a string, b string) *Element {
count++
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b")))
secondRender := sortHtmlAttributes(Render(page("a", "b")))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
}
func TestCachedT3(t *testing.T) {
t.Parallel()
count := 0
page := CachedT3(time.Hour, func(a string, b string, c string) *Element {
count++
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b", "c")))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c")))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
}
func TestCachedT4(t *testing.T) {
t.Parallel()
count := 0
page := CachedT4(time.Hour, func(a string, b string, c string, d string) *Element {
count++
return ComplexPage()
})
firstRender := sortHtmlAttributes(Render(page("a", "b", "c", "d")))
secondRender := sortHtmlAttributes(Render(page("a", "b", "c", "d")))
assert.Equal(t, firstRender, secondRender)
assert.Equal(t, 1, count)
assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage())))
}
func TestCachedExpired(t *testing.T) { func TestCachedExpired(t *testing.T) {
t.Parallel() t.Parallel()
count := 0 count := 0
@ -388,9 +453,82 @@ func TestCacheByKeyT1Expired_2(t *testing.T) {
assert.Equal(t, 3, renderCount) assert.Equal(t, 3, renderCount)
} }
func TestClearExpiredCached(t *testing.T) {
t.Parallel()
renderCount := 0
cachedItem := Cached(time.Millisecond*3, func() *Element {
renderCount++
return Pf("hello")
})
Render(cachedItem())
Render(cachedItem())
node := cachedItem().meta.(*CachedNode)
assert.Equal(t, 1, renderCount)
assert.NotEmpty(t, node.html)
time.Sleep(time.Millisecond * 3)
node.ClearExpired()
assert.Empty(t, node.html)
}
func TestClearExpiredCacheByKey(t *testing.T) {
t.Parallel()
renderCount := 0
cachedItem := CachedPerKeyT(time.Millisecond, func(key int) (any, GetElementFunc) {
return key, func() *Element {
renderCount++
return Pf(strconv.Itoa(key))
}
})
for i := 0; i < 100; i++ {
Render(cachedItem(i))
}
node := cachedItem(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode)
assert.Equal(t, 100, len(node.byKeyExpiration))
assert.Equal(t, 100, len(node.byKeyCache))
time.Sleep(time.Millisecond * 2)
Render(cachedItem(0))
node.ClearExpired()
assert.Equal(t, 1, len(node.byKeyExpiration))
assert.Equal(t, 1, len(node.byKeyCache))
node.ClearCache()
assert.Equal(t, 0, len(node.byKeyExpiration))
assert.Equal(t, 0, len(node.byKeyCache))
}
func TestBackgroundCleaner(t *testing.T) {
t.Parallel()
cachedItem := CachedPerKeyT(time.Second*2, func(key int) (any, GetElementFunc) {
return key, func() *Element {
return Pf(strconv.Itoa(key))
}
})
for i := 0; i < 100; i++ {
Render(cachedItem(i))
}
node := cachedItem(0).meta.(*ByKeyEntry).parent.meta.(*CachedNode)
assert.Equal(t, 100, len(node.byKeyExpiration))
assert.Equal(t, 100, len(node.byKeyCache))
time.Sleep(time.Second * 3)
assert.Equal(t, 0, len(node.byKeyExpiration))
assert.Equal(t, 0, len(node.byKeyCache))
}
func BenchmarkCacheByKey(b *testing.B) { func BenchmarkCacheByKey(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
page := CachedPerKeyT(time.Hour, func(userId string) (any, GetElementFunc) { page := CachedPerKeyT(time.Second*3, func(userId string) (any, GetElementFunc) {
return userId, func() *Element { return userId, func() *Element {
return MailTo(userId) return MailTo(userId)
} }