diff --git a/relay/cache/cache.go b/relay/cache/cache.go index 90bc653..5d4fee0 100644 --- a/relay/cache/cache.go +++ b/relay/cache/cache.go @@ -8,11 +8,13 @@ import ( type item struct { value interface{} expiration int64 + staleTime int64 } type Cache struct { - items map[string]item - mu sync.RWMutex + items map[string]item + mu sync.RWMutex + refreshing sync.Map } func New() *Cache { @@ -27,35 +29,68 @@ func (c *Cache) Set(key string, value interface{}, duration time.Duration) { c.mu.Lock() defer c.mu.Unlock() + now := time.Now().UnixNano() c.items[key] = item{ value: value, - expiration: time.Now().Add(duration).UnixNano(), + expiration: now + duration.Nanoseconds(), + staleTime: now + (duration * 2).Nanoseconds(), // Stale time is double the normal duration } } func (c *Cache) Get(key string) (interface{}, bool) { c.mu.RLock() - defer c.mu.RUnlock() - item, exists := c.items[key] + c.mu.RUnlock() + if !exists { return nil, false } - if time.Now().UnixNano() > item.expiration { + now := time.Now().UnixNano() + if now > item.staleTime { return nil, false } return item.value, true } +func (c *Cache) GetOrRefresh(key string, refreshFn func() (interface{}, error), duration time.Duration) (interface{}, bool) { + // Try to get from cache first + if value, exists := c.Get(key); exists { + now := time.Now().UnixNano() + item := c.items[key] + + // If the value is expired but not stale, trigger refresh in background + if now > item.expiration && now <= item.staleTime { + if _, loading := c.refreshing.LoadOrStore(key, true); !loading { + go func() { + defer c.refreshing.Delete(key) + if newValue, err := refreshFn(); err == nil { + c.Set(key, newValue, duration) + } + }() + } + } + return value, true + } + + // If no value exists, do a blocking refresh + value, err := refreshFn() + if err != nil { + return nil, false + } + + c.Set(key, value, duration) + return value, true +} + func (c *Cache) startCleanup() { ticker := time.NewTicker(time.Minute) for range ticker.C { c.mu.Lock() now := time.Now().UnixNano() for k, v := range c.items { - if now > v.expiration { + if now > v.staleTime { delete(c.items, k) } } diff --git a/relay/trending/kinds.go b/relay/trending/kinds.go index ca394f9..c95f070 100644 --- a/relay/trending/kinds.go +++ b/relay/trending/kinds.go @@ -23,12 +23,7 @@ var ( cacheDuration = 5 * time.Minute ) -// GetTrendingKind20 returns the top 20 trending posts of kind 20 from the last 24 hours -func GetTrendingKind20(db *sql.DB) ([]Post, error) { - if cached, ok := trendingCache.Get("trending_kind_20"); ok { - return cached.([]Post), nil - } - +func fetchTrendingKind20(db *sql.DB) ([]Post, error) { query := ` WITH reactions AS ( SELECT @@ -76,6 +71,17 @@ func GetTrendingKind20(db *sql.DB) ([]Post, error) { trendingPosts = append(trendingPosts, post) } - trendingCache.Set("trending_kind_20", trendingPosts, cacheDuration) return trendingPosts, nil } + +func GetTrendingKind20(db *sql.DB) ([]Post, error) { + posts, exists := trendingCache.GetOrRefresh("trending_kind_20", func() (interface{}, error) { + return fetchTrendingKind20(db) + }, cacheDuration) + + if !exists { + return nil, nil + } + + return posts.([]Post), nil +}