Files
qwqcode 0b44989e5f fix(cache): return database results on store failure
Treat cache writes as best-effort after a successful database query. Keep database errors authoritative and cover both paths with a failing store.

Fixes #1154
2026-07-22 22:20:28 +08:00

89 lines
1.8 KiB
Go

package cache
import (
"fmt"
"reflect"
"github.com/artalkjs/artalk/v2/internal/log"
"github.com/eko/gocache/lib/v4/store"
"golang.org/x/sync/singleflight"
)
var cacheSingleflightGroup = new(singleflight.Group)
func QueryDBWithCache[T any](c *Cache, name string, queryDB func() (T, error)) (T, error) {
// Use SingleFlight to prevent Cache Breakdown
v, err, _ := cacheSingleflightGroup.Do(name, func() (any, error) {
var val T
// Query from cache
err := c.FindCache(name, &val)
if err != nil {
// Miss cache
// Query from db
val, err := queryDB()
if err != nil {
return nil, err
}
// Store cache
if err := c.StoreCache(val, name); err != nil {
log.Warn("[Cache] failed to store database query result: ", err)
}
return val, nil
} else {
// Hit cache
return val, nil
}
})
if err != nil {
return *new(T), err
}
return v.(T), err
}
func (c *Cache) FindCache(name string, dest any) error {
if reflect.ValueOf(dest).Kind() != reflect.Ptr {
return fmt.Errorf("[FindCache] dest must be a pointer")
}
// `Get()` is Thread Safe, so no need to add Mutex
// @see https://github.com/go-redis/redis/issues/23
_, err := c.marshal.Get(c.ctx, name, dest)
if err != nil {
log.Debug("[CacheMis] " + name)
return err
}
log.Debug("[CacheHit] " + name)
return nil
}
func (c *Cache) StoreCache(source any, name ...string) (err error) {
for _, n := range name {
log.Debug("[StoreCache] " + n)
// `Set()` is Thread Safe, no need to add Mutex
if setErr := c.marshal.Set(c.ctx, n, source,
store.WithExpiration(c.ttl),
); setErr != nil {
err = setErr
}
}
return
}
func (c *Cache) DelCache(name ...string) (err error) {
for _, n := range name {
log.Debug("[DelCache] " + n)
if delErr := c.marshal.Delete(c.ctx, n); delErr != nil {
err = delErr
}
}
return
}