fix expiration for non by key cache
add goroutine to clean up old cache entries to save mem
This commit is contained in:
parent
beeb8bbe30
commit
af81f53160
2 changed files with 212 additions and 16 deletions
|
|
@ -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{
|
||||||
|
|
@ -173,10 +189,11 @@ func CachedT[T any](duration time.Duration, cb GetElementFuncT[T]) func(T) *Elem
|
||||||
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)
|
||||||
|
|
@ -190,9 +207,10 @@ func CachedT2[T any, T2 any](duration time.Duration, cb GetElementFuncT2[T, T2])
|
||||||
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)
|
||||||
|
|
@ -206,9 +224,10 @@ func CachedT3[T any, T2 any, T3 any](duration time.Duration, cb GetElementFuncT3
|
||||||
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)
|
||||||
|
|
@ -222,9 +241,10 @@ func CachedT4[T any, T2 any, T3 any, T4 any](duration time.Duration, cb GetEleme
|
||||||
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue