mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
381c79417e
Four long-running goroutines were launched with a bare go statement, so a
panic in any of them took down the whole process: the RAM cache's expired-key
eviction, the batch writer's flush worker, the disk cache cleanup worker and
the PoW memory store sweeper. Route them through util.Go.
The architecture gate only grepped for 'go func(', which is why the named-call
form went unnoticed; widen it to cover both launch styles.
235 lines
5.6 KiB
Go
235 lines
5.6 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package ram
|
|
|
|
import (
|
|
"Wavelet/pkg/util"
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// ErrNotFound is returned by the Loader when the requested item is not found.
|
|
ErrNotFound = errors.New("cache item not found in data source")
|
|
|
|
managerCache *Cache[string, map[string]cacheEntry]
|
|
|
|
writeLocks = make(map[string]*sync.Mutex)
|
|
writeLocksMu sync.Mutex
|
|
)
|
|
|
|
// CacheItem represents a unified cache entity.
|
|
type CacheItem struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Type string `json:"type"`
|
|
TTL time.Duration `json:"ttl"` // -1 means never expire
|
|
}
|
|
|
|
// Loader is an interface that the cache client must implement to handle database retrieval.
|
|
type Loader interface {
|
|
LoadAll(ctx context.Context, configType string) ([]CacheItem, error)
|
|
LoadOne(ctx context.Context, configType, key string) (CacheItem, error)
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
item CacheItem
|
|
expireAt time.Time
|
|
}
|
|
|
|
func init() {
|
|
// Initialize with a large maximum size since it only stores one entry per configType
|
|
managerCache = MustNew[string, map[string]cacheEntry](Options{
|
|
MaximumSize: 1000,
|
|
})
|
|
}
|
|
|
|
func getWriteLock(configType string) *sync.Mutex {
|
|
writeLocksMu.Lock()
|
|
defer writeLocksMu.Unlock()
|
|
lock, found := writeLocks[configType]
|
|
if !found {
|
|
lock = &sync.Mutex{}
|
|
writeLocks[configType] = lock
|
|
}
|
|
return lock
|
|
}
|
|
|
|
// Get retrieves a cache item from the local cache store, checking for expiration.
|
|
// Reads are completely lock-free because maps stored in Otter are immutable.
|
|
func Get(configType, key string) (CacheItem, bool) {
|
|
m, ok := managerCache.GetIfPresent(configType)
|
|
if !ok {
|
|
return CacheItem{}, false
|
|
}
|
|
|
|
entry, found := m[key]
|
|
if !found {
|
|
return CacheItem{}, false
|
|
}
|
|
|
|
// Check expiration
|
|
if entry.item.TTL != -1 && !entry.expireAt.IsZero() && time.Now().After(entry.expireAt) {
|
|
// Asynchronously remove the expired item from the map and write back
|
|
util.Go(func() { deleteKeyIfExpired(configType, key, entry.expireAt) })
|
|
return CacheItem{}, false
|
|
}
|
|
|
|
return entry.item, true
|
|
}
|
|
|
|
func deleteKeyIfExpired(configType, key string, expireAt time.Time) {
|
|
lock := getWriteLock(configType)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
currentMap, ok := managerCache.GetIfPresent(configType)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
entry, found := currentMap[key]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
// Double-check expiration time to ensure we don't delete a newly updated key
|
|
if entry.expireAt != expireAt || !time.Now().After(entry.expireAt) {
|
|
return
|
|
}
|
|
|
|
newMap := make(map[string]cacheEntry, len(currentMap)-1)
|
|
for k, v := range currentMap {
|
|
if k != key {
|
|
newMap[k] = v
|
|
}
|
|
}
|
|
managerCache.Set(configType, newMap)
|
|
}
|
|
|
|
// Set stores a cache item in the local cache store.
|
|
// Writes are protected by a fine-grained lock per configType.
|
|
func Set(item CacheItem) {
|
|
lock := getWriteLock(item.Type)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
currentMap, ok := managerCache.GetIfPresent(item.Type)
|
|
newMap := make(map[string]cacheEntry)
|
|
if ok {
|
|
for k, v := range currentMap {
|
|
newMap[k] = v
|
|
}
|
|
}
|
|
|
|
var expireAt time.Time
|
|
if item.TTL != -1 {
|
|
expireAt = time.Now().Add(item.TTL)
|
|
}
|
|
|
|
newMap[item.Key] = cacheEntry{
|
|
item: item,
|
|
expireAt: expireAt,
|
|
}
|
|
managerCache.Set(item.Type, newMap)
|
|
}
|
|
|
|
// Delete removes a single item from the local cache store.
|
|
// Writes are protected by a fine-grained lock per configType.
|
|
func Delete(configType, key string) {
|
|
lock := getWriteLock(configType)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
currentMap, ok := managerCache.GetIfPresent(configType)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
newMap := make(map[string]cacheEntry, len(currentMap))
|
|
for k, v := range currentMap {
|
|
if k != key {
|
|
newMap[k] = v
|
|
}
|
|
}
|
|
managerCache.Set(configType, newMap)
|
|
}
|
|
|
|
// UpdateTypeItems replaces all cache items of a specific type atomically.
|
|
// Writes are protected by a fine-grained lock per configType.
|
|
func UpdateTypeItems(configType string, items []CacheItem) {
|
|
lock := getWriteLock(configType)
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
newMap := make(map[string]cacheEntry, len(items))
|
|
for _, item := range items {
|
|
var expireAt time.Time
|
|
if item.TTL != -1 {
|
|
expireAt = time.Now().Add(item.TTL)
|
|
}
|
|
newMap[item.Key] = cacheEntry{
|
|
item: item,
|
|
expireAt: expireAt,
|
|
}
|
|
}
|
|
managerCache.Set(configType, newMap)
|
|
}
|
|
|
|
// GetTypeItems retrieves all unexpired cache items of a specific type.
|
|
// Reads are completely lock-free because maps stored in Otter are immutable.
|
|
func GetTypeItems(configType string) []CacheItem {
|
|
currentMap, ok := managerCache.GetIfPresent(configType)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var list []CacheItem
|
|
for _, entry := range currentMap {
|
|
if entry.item.TTL == -1 || entry.expireAt.IsZero() || time.Now().Before(entry.expireAt) {
|
|
list = append(list, entry.item)
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
// Refresh reloads configuration cache from database via the Loader.
|
|
func Refresh(ctx context.Context, configType, key string, loader Loader) error {
|
|
if configType == "" {
|
|
return errors.New("type is required")
|
|
}
|
|
|
|
if key != "" {
|
|
// Single key refresh: first fetch latest value from database
|
|
item, err := loader.LoadOne(ctx, configType, key)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
Delete(configType, key)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
Set(item)
|
|
return nil
|
|
}
|
|
|
|
// All keys refresh: load all of that type from database first, then replace cache
|
|
items, err := loader.LoadAll(ctx, configType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
UpdateTypeItems(configType, items)
|
|
return nil
|
|
}
|
|
|
|
// ResetForTest clears the local store and locks.
|
|
func ResetForTest() {
|
|
writeLocksMu.Lock()
|
|
writeLocks = make(map[string]*sync.Mutex)
|
|
writeLocksMu.Unlock()
|
|
managerCache.InvalidateAll()
|
|
}
|