mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
refactor(oauth): replace legacy oauth cache with standard ram cache and add pubsub synchronization
- Replaced custom map-based cache in apps/oauth/cache.go with standard pkg/cache/ram framework. - Implemented Redis Pub/Sub invalidation channels for distributed token and user cache synchronization. - Created apps/oauth/cache_test.go to verify local cache operations and pub/sub broadcasts. refactor(cache): generic RAM cache with CoW and unified preheating Replaced L2 Redis cache and old cache package with process-local generic pkg/cache/ram. Implemented Copy-on-Write for reads, fine-grained locks per type for writes, and unified preheating in bootstrap. Changed cache invalidation to lazy-loading to resolve SQLite deadlocks during transactions.
This commit is contained in:
@@ -11,14 +11,15 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
||||
)
|
||||
|
||||
const (
|
||||
configTypeSystem = "system"
|
||||
errDatabaseNotInitialized = "database not initialized"
|
||||
errConfigIntParseFailed = "配置 %s 的值 '%s' 无法转换为整数: %w"
|
||||
errConfigDecimalParseFailed = "配置 %s 的值 '%s' 无法转换为decimal: %w"
|
||||
@@ -26,21 +27,44 @@ const (
|
||||
errParseMenuDisplayConfigFailed = "解析目录显示配置失败: %w"
|
||||
)
|
||||
|
||||
// GetSystemConfigByKey 通过 key 查询配置(带 RAM + Redis 缓存)。
|
||||
func GetSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
||||
ensureSystemConfigCacheListener()
|
||||
// PreheatSystemConfigs loads all system configs from database.
|
||||
// This function strictly performs database read and does not perform any cache read or write operations.
|
||||
func PreheatSystemConfigs(ctx context.Context) ([]model.SystemConfig, error) {
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return nil, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
if cached, ok := systemConfigRAMCache.GetIfPresent(key); ok {
|
||||
return cloneSystemConfig(cached), nil
|
||||
var configs []model.SystemConfig
|
||||
if err := database.Find(&configs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// PreheatSystemConfigByKey loads a single config key from database.
|
||||
// This function strictly performs database read and does not perform any cache read or write operations.
|
||||
func PreheatSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return model.SystemConfig{}, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
var sc model.SystemConfig
|
||||
if db.Redis != nil {
|
||||
if err := db.HGetJSON(ctx, SystemConfigRedisHashKey, key, &sc); err == nil {
|
||||
systemConfigRAMCache.Set(key, cloneSystemConfig(sc))
|
||||
if err := database.Where("key = ?", key).First(&sc).Error; err != nil {
|
||||
return model.SystemConfig{}, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// GetSystemConfigByGroup queries a configuration by Type and Key.
|
||||
func GetSystemConfigByGroup(ctx context.Context, configType string, key string) (model.SystemConfig, error) {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
if item, ok := ram.Get(configType, key); ok {
|
||||
var sc model.SystemConfig
|
||||
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
||||
return sc, nil
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return model.SystemConfig{}, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,15 +73,31 @@ func GetSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig,
|
||||
return model.SystemConfig{}, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
var sc model.SystemConfig
|
||||
if err := database.Where("key = ?", key).First(&sc).Error; err != nil {
|
||||
return model.SystemConfig{}, err
|
||||
}
|
||||
|
||||
populateSystemConfigCache(ctx, sc)
|
||||
// Populate local cache directly on query miss
|
||||
valBytes, err := json.Marshal(sc)
|
||||
if err == nil {
|
||||
ram.Set(ram.CacheItem{
|
||||
Key: sc.Key,
|
||||
Value: string(valBytes),
|
||||
Type: configType,
|
||||
TTL: determineTTL(sc.Key),
|
||||
})
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// ListSystemConfigsByKeys loads multiple config keys in one database round trip.
|
||||
// GetSystemConfigByKey queries config by key (delegates to Type "config").
|
||||
func GetSystemConfigByKey(ctx context.Context, key string) (model.SystemConfig, error) {
|
||||
return GetSystemConfigByGroup(ctx, ConfigCacheType, key)
|
||||
}
|
||||
|
||||
// ListSystemConfigsByKeys loads multiple config keys.
|
||||
func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]model.SystemConfig, error) {
|
||||
if len(keys) == 0 {
|
||||
return map[string]model.SystemConfig{}, nil
|
||||
@@ -67,28 +107,16 @@ func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]mod
|
||||
|
||||
result := make(map[string]model.SystemConfig, len(keys))
|
||||
missing := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if cached, ok := systemConfigRAMCache.GetIfPresent(key); ok {
|
||||
result[key] = cloneSystemConfig(cached)
|
||||
continue
|
||||
}
|
||||
missing = append(missing, key)
|
||||
}
|
||||
|
||||
if len(missing) > 0 && db.Redis != nil {
|
||||
stillMissing := make([]string, 0, len(missing))
|
||||
for _, key := range missing {
|
||||
for _, key := range keys {
|
||||
if item, ok := ram.Get(ConfigCacheType, key); ok {
|
||||
var sc model.SystemConfig
|
||||
if err := db.HGetJSON(ctx, SystemConfigRedisHashKey, key, &sc); err == nil {
|
||||
systemConfigRAMCache.Set(key, cloneSystemConfig(sc))
|
||||
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
||||
result[key] = sc
|
||||
continue
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
stillMissing = append(stillMissing, key)
|
||||
}
|
||||
missing = stillMissing
|
||||
missing = append(missing, key)
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
@@ -106,8 +134,16 @@ func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]mod
|
||||
}
|
||||
|
||||
for i := range configs {
|
||||
populateSystemConfigCache(ctx, configs[i])
|
||||
result[configs[i].Key] = cloneSystemConfig(configs[i])
|
||||
valBytes, err := json.Marshal(configs[i])
|
||||
if err == nil {
|
||||
ram.Set(ram.CacheItem{
|
||||
Key: configs[i].Key,
|
||||
Value: string(valBytes),
|
||||
Type: ConfigCacheType,
|
||||
TTL: determineTTL(configs[i].Key),
|
||||
})
|
||||
}
|
||||
result[configs[i].Key] = configs[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -115,21 +151,25 @@ func ListSystemConfigsByKeys(ctx context.Context, keys []string) (map[string]mod
|
||||
|
||||
// InvalidateVisibleSystemConfigsCache clears the cached public config list.
|
||||
func InvalidateVisibleSystemConfigsCache(ctx context.Context) error {
|
||||
if db.Redis == nil {
|
||||
return nil
|
||||
}
|
||||
return db.Redis.Del(ctx, db.PrefixedKey(SystemConfigVisibleListRedisKey)).Err()
|
||||
return InvalidateAllSystemConfigCaches(ctx)
|
||||
}
|
||||
|
||||
// ListVisibleSystemConfigs 查询所有可通过公共配置接口暴露的配置(带 Redis 列表缓存)。
|
||||
// ListVisibleSystemConfigs queries visible configs using local cache store.
|
||||
func ListVisibleSystemConfigs(ctx context.Context) ([]model.SystemConfig, error) {
|
||||
if db.Redis != nil {
|
||||
var cached []model.SystemConfig
|
||||
if err := db.GetJSON(ctx, SystemConfigVisibleListRedisKey, &cached); err == nil {
|
||||
return cached, nil
|
||||
} else if !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
items := ram.GetTypeItems(ConfigCacheType)
|
||||
if len(items) > 0 {
|
||||
var list []model.SystemConfig
|
||||
for _, item := range items {
|
||||
var sc model.SystemConfig
|
||||
if err := json.Unmarshal([]byte(item.Value), &sc); err == nil {
|
||||
if sc.Visibility == model.ConfigVisibilityVisible {
|
||||
list = append(list, sc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
database := db.DB(ctx)
|
||||
@@ -142,14 +182,24 @@ func ListVisibleSystemConfigs(ctx context.Context) ([]model.SystemConfig, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if db.Redis != nil {
|
||||
_ = db.SetJSON(ctx, SystemConfigVisibleListRedisKey, configs, 0)
|
||||
// Populate visible configs to local cache store
|
||||
for _, cfg := range configs {
|
||||
valBytes, err := json.Marshal(cfg)
|
||||
if err == nil {
|
||||
ram.Set(ram.CacheItem{
|
||||
Key: cfg.Key,
|
||||
Value: string(valBytes),
|
||||
Type: ConfigCacheType,
|
||||
TTL: determineTTL(cfg.Key),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// GetIntByKey 通过 key 查询配置并转换为 int 类型。
|
||||
|
||||
// GetIntByKey queries config and converts to int.
|
||||
func GetIntByKey(ctx context.Context, key string) (int, error) {
|
||||
sc, err := GetSystemConfigByKey(ctx, key)
|
||||
if err != nil {
|
||||
@@ -164,7 +214,7 @@ func GetIntByKey(ctx context.Context, key string) (int, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetDecimalByKey 通过 key 查询配置并转换为 decimal.Decimal 类型。
|
||||
// GetDecimalByKey queries config and converts to decimal.Decimal.
|
||||
func GetDecimalByKey(ctx context.Context, key string, precision int32) (decimal.Decimal, error) {
|
||||
sc, err := GetSystemConfigByKey(ctx, key)
|
||||
if err != nil {
|
||||
@@ -179,7 +229,7 @@ func GetDecimalByKey(ctx context.Context, key string, precision int32) (decimal.
|
||||
return value.Truncate(precision), nil
|
||||
}
|
||||
|
||||
// GetBoolByKey 通过 key 查询配置并转换为 bool 类型。
|
||||
// GetBoolByKey queries config and converts to bool.
|
||||
func GetBoolByKey(ctx context.Context, key string) (bool, error) {
|
||||
sc, err := GetSystemConfigByKey(ctx, key)
|
||||
if err != nil {
|
||||
@@ -194,7 +244,7 @@ func GetBoolByKey(ctx context.Context, key string) (bool, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetMenuDisplayConfig 获取目录显示配置,解析为 map[string]bool。
|
||||
// GetMenuDisplayConfig queries and parses menu config.
|
||||
func GetMenuDisplayConfig(ctx context.Context) (map[string]bool, error) {
|
||||
sc, err := GetSystemConfigByKey(ctx, model.ConfigKeyMenuDisplayConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -69,7 +69,7 @@ func SaveOrUpdateSystemConfig(ctx context.Context, key, value string) error {
|
||||
sc = model.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: "system",
|
||||
Type: configTypeSystem,
|
||||
Visibility: model.ConfigVisibilityHidden,
|
||||
}
|
||||
if err := db.DB(ctx).Create(&sc).Error; err != nil {
|
||||
|
||||
@@ -6,31 +6,87 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/cache/ram"
|
||||
)
|
||||
|
||||
const (
|
||||
// SystemConfigInvalidationChannel broadcasts RAM cache eviction across nodes.
|
||||
SystemConfigInvalidationChannel = "system:config_invalidation"
|
||||
// SystemConfigRedisHashKey Redis Hash key,存储所有系统配置。
|
||||
// SystemConfigBroadcastChannel broadcasts system config cache updates across nodes.
|
||||
SystemConfigBroadcastChannel = "system:config_broadcast"
|
||||
|
||||
// SystemConfigInvalidationChannel is kept as an alias for backward compatibility.
|
||||
SystemConfigInvalidationChannel = SystemConfigBroadcastChannel
|
||||
|
||||
// SystemConfigRedisHashKey is kept for backward compatibility in tests.
|
||||
SystemConfigRedisHashKey = "system:system_configs"
|
||||
// SystemConfigVisibleListRedisKey Redis key,缓存所有 visibility=1 的公共配置列表。
|
||||
// SystemConfigVisibleListRedisKey is kept for backward compatibility in tests.
|
||||
SystemConfigVisibleListRedisKey = "system:visible_configs"
|
||||
|
||||
systemConfigInvalidateAllToken = "*"
|
||||
systemConfigRAMMaximumSize = 512
|
||||
// ConfigCacheType is the cache type for all system configs.
|
||||
ConfigCacheType = "config"
|
||||
)
|
||||
|
||||
type systemConfigInvalidationMessage struct {
|
||||
Key string `json:"key"`
|
||||
type systemConfigBroadcastMessage struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// ConfigLoader loads configuration data from the database.
|
||||
type ConfigLoader struct{}
|
||||
|
||||
// LoadAll loads all system configs from database as CacheItems.
|
||||
func (ConfigLoader) LoadAll(ctx context.Context, configType string) ([]ram.CacheItem, error) {
|
||||
configs, err := PreheatSystemConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]ram.CacheItem, len(configs))
|
||||
for i, cfg := range configs {
|
||||
valBytes, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items[i] = ram.CacheItem{
|
||||
Key: cfg.Key,
|
||||
Value: string(valBytes),
|
||||
Type: configType,
|
||||
TTL: determineTTL(cfg.Key),
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// LoadOne loads a single system config from database as a CacheItem.
|
||||
func (ConfigLoader) LoadOne(ctx context.Context, configType string, key string) (ram.CacheItem, error) {
|
||||
cfg, err := PreheatSystemConfigByKey(ctx, key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ram.CacheItem{}, ram.ErrNotFound
|
||||
}
|
||||
return ram.CacheItem{}, err
|
||||
}
|
||||
|
||||
valBytes, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return ram.CacheItem{}, err
|
||||
}
|
||||
|
||||
return ram.CacheItem{
|
||||
Key: cfg.Key,
|
||||
Value: string(valBytes),
|
||||
Type: configType,
|
||||
TTL: determineTTL(cfg.Key),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
systemConfigRAMCache = ram.MustNew[string, model.SystemConfig](ram.Options{MaximumSize: systemConfigRAMMaximumSize})
|
||||
systemConfigListenerOnce sync.Once
|
||||
systemConfigListenerCtx context.Context
|
||||
systemConfigListenerCancel context.CancelFunc
|
||||
@@ -48,7 +104,7 @@ func startSystemConfigCacheInvalidationListener() {
|
||||
systemConfigListenerCtx, systemConfigListenerCancel = context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
pubsub := db.Redis.Subscribe(systemConfigListenerCtx, SystemConfigInvalidationChannel)
|
||||
pubsub := db.Redis.Subscribe(systemConfigListenerCtx, SystemConfigBroadcastChannel)
|
||||
defer func() {
|
||||
_ = pubsub.Close()
|
||||
}()
|
||||
@@ -59,16 +115,18 @@ func startSystemConfigCacheInvalidationListener() {
|
||||
}()
|
||||
|
||||
for msg := range pubsub.Channel() {
|
||||
var payload systemConfigInvalidationMessage
|
||||
var payload systemConfigBroadcastMessage
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
ram.UpdateTypeItems(ConfigCacheType, nil)
|
||||
continue
|
||||
}
|
||||
if payload.Key == "" || payload.Key == systemConfigInvalidateAllToken {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
continue
|
||||
|
||||
key := payload.Key
|
||||
if key == "*" || key == "" {
|
||||
ram.UpdateTypeItems(payload.Type, nil)
|
||||
} else {
|
||||
ram.Delete(payload.Type, key)
|
||||
}
|
||||
systemConfigRAMCache.Invalidate(payload.Key)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -82,57 +140,53 @@ func StopSystemConfigCacheListener() {
|
||||
systemConfigListenerOnce = sync.Once{}
|
||||
}
|
||||
|
||||
func cloneSystemConfig(sc model.SystemConfig) model.SystemConfig {
|
||||
return sc
|
||||
func determineTTL(_ string) time.Duration {
|
||||
// Program-determined TTL: -1 means never expire for all configs by default
|
||||
return -1
|
||||
}
|
||||
|
||||
func populateSystemConfigCache(ctx context.Context, sc model.SystemConfig) {
|
||||
systemConfigRAMCache.Set(sc.Key, cloneSystemConfig(sc))
|
||||
if db.Redis != nil {
|
||||
_ = db.HSetJSON(ctx, SystemConfigRedisHashKey, sc.Key, &sc)
|
||||
}
|
||||
}
|
||||
|
||||
func publishSystemConfigRAMInvalidation(ctx context.Context, key string) {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(systemConfigInvalidationMessage{Key: key})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Publish(ctx, SystemConfigInvalidationChannel, payload).Err()
|
||||
}
|
||||
|
||||
// InvalidateSystemConfigCache evicts one config key from local RAM and Redis.
|
||||
// InvalidateSystemConfigCache triggers a broadcast to refresh the cache for key.
|
||||
func InvalidateSystemConfigCache(ctx context.Context, key string) error {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
systemConfigRAMCache.Invalidate(key)
|
||||
// Invalidate local cache synchronously first
|
||||
ram.Delete(ConfigCacheType, key)
|
||||
|
||||
// Broadcast to other nodes and clean legacy Redis cache key
|
||||
if db.Redis != nil {
|
||||
if err := db.HDel(ctx, SystemConfigRedisHashKey, key); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = db.HDel(ctx, SystemConfigRedisHashKey, key)
|
||||
publishSystemConfigBroadcast(ctx, ConfigCacheType, key)
|
||||
}
|
||||
publishSystemConfigRAMInvalidation(ctx, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InvalidateAllSystemConfigCaches evicts all config entries from local RAM and Redis.
|
||||
// InvalidateAllSystemConfigCaches triggers a broadcast to refresh the entire config cache.
|
||||
func InvalidateAllSystemConfigCaches(ctx context.Context) error {
|
||||
ensureSystemConfigCacheListener()
|
||||
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
// Invalidate all items of type ConfigCacheType synchronously first
|
||||
ram.UpdateTypeItems(ConfigCacheType, nil)
|
||||
|
||||
// Broadcast to other nodes and clean legacy Redis cache keys
|
||||
if db.Redis != nil {
|
||||
if err := db.Redis.Del(ctx, db.PrefixedKey(SystemConfigRedisHashKey)).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = db.Redis.Del(ctx, db.PrefixedKey(SystemConfigRedisHashKey), db.PrefixedKey(SystemConfigVisibleListRedisKey)).Err()
|
||||
publishSystemConfigBroadcast(ctx, ConfigCacheType, "*")
|
||||
}
|
||||
publishSystemConfigRAMInvalidation(ctx, systemConfigInvalidateAllToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishSystemConfigBroadcast(ctx context.Context, configType string, key string) {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
payload, err := json.Marshal(systemConfigBroadcastMessage{Type: configType, Key: key})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Publish(ctx, SystemConfigBroadcastChannel, payload).Err()
|
||||
}
|
||||
|
||||
// ResetSystemConfigRAMCacheForTest clears only the process-local RAM cache.
|
||||
func ResetSystemConfigRAMCacheForTest() {
|
||||
systemConfigRAMCache.InvalidateAll()
|
||||
ram.ResetForTest()
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/maintnotifications"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
func setupSystemConfigTest(t *testing.T) (*gorm.DB, func()) {
|
||||
@@ -54,6 +56,7 @@ func setupSystemConfigTest(t *testing.T) (*gorm.DB, func()) {
|
||||
db.SetDB(sqliteDB)
|
||||
db.Redis = redisClient
|
||||
|
||||
|
||||
cleanup := func() {
|
||||
StopSystemConfigCacheListener()
|
||||
ResetSystemConfigRAMCacheForTest()
|
||||
@@ -76,16 +79,14 @@ func TestListSystemConfigsByKeys_EmptyKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) {
|
||||
func TestListSystemConfigsByKeys_LoadsFromRAMCache(t *testing.T) {
|
||||
dbConn, cleanup := setupSystemConfigTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
ResetSystemConfigRAMCacheForTest()
|
||||
if err := InvalidateAllSystemConfigCaches(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err)
|
||||
}
|
||||
|
||||
// Initial load
|
||||
warm, err := GetSystemConfigByKey(ctx, model.ConfigKeySiteName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSystemConfigByKey(site_name) warm error = %v", err)
|
||||
@@ -94,14 +95,14 @@ func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) {
|
||||
t.Fatalf("GetSystemConfigByKey(site_name).Value = %q, want %q", warm.Value, "Wavelet")
|
||||
}
|
||||
|
||||
// Update DB directly
|
||||
if err := dbConn.Model(&model.SystemConfig{}).
|
||||
Where("key = ?", model.ConfigKeySiteName).
|
||||
Update("value", "db_only_value").Error; err != nil {
|
||||
t.Fatalf("Update(site_name) error = %v", err)
|
||||
}
|
||||
|
||||
ResetSystemConfigRAMCacheForTest()
|
||||
|
||||
// Fetch via ListSystemConfigsByKeys should serve from local store (meaning the old value "Wavelet")
|
||||
configs, err := ListSystemConfigsByKeys(ctx, []string{model.ConfigKeySiteName})
|
||||
if err != nil {
|
||||
t.Fatalf("ListSystemConfigsByKeys(site_name) error = %v", err)
|
||||
@@ -112,35 +113,47 @@ func TestListSystemConfigsByKeys_LoadsFromRedisBeforeDB(t *testing.T) {
|
||||
t.Fatal("ListSystemConfigsByKeys(site_name) missing site_name entry")
|
||||
}
|
||||
if sc.Value != "Wavelet" {
|
||||
t.Fatalf("ListSystemConfigsByKeys(site_name).Value = %q, want redis value %q", sc.Value, "Wavelet")
|
||||
t.Fatalf("ListSystemConfigsByKeys(site_name).Value = %q, want cached value %q", sc.Value, "Wavelet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSystemConfigsByKeys_PopulatesRAMFromRedis(t *testing.T) {
|
||||
_, cleanup := setupSystemConfigTest(t)
|
||||
func TestGetSystemConfigByGroupAndInvalidation(t *testing.T) {
|
||||
dbConn, cleanup := setupSystemConfigTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
ResetSystemConfigRAMCacheForTest()
|
||||
if err := InvalidateAllSystemConfigCaches(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err)
|
||||
|
||||
// Get via specific group/type
|
||||
cfg, err := GetSystemConfigByGroup(ctx, ConfigCacheType, model.ConfigKeySiteName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSystemConfigByGroup error = %v", err)
|
||||
}
|
||||
if cfg.Value != "Wavelet" {
|
||||
t.Fatalf("value = %q, want %q", cfg.Value, "Wavelet")
|
||||
}
|
||||
|
||||
if _, err := GetSystemConfigByKey(ctx, model.ConfigKeySiteName); err != nil {
|
||||
t.Fatalf("GetSystemConfigByKey(site_name) warm error = %v", err)
|
||||
// Direct DB update
|
||||
if err := dbConn.Model(&model.SystemConfig{}).
|
||||
Where("key = ?", model.ConfigKeySiteName).
|
||||
Update("value", "new_site_name").Error; err != nil {
|
||||
t.Fatalf("DB Update error = %v", err)
|
||||
}
|
||||
|
||||
ResetSystemConfigRAMCacheForTest()
|
||||
|
||||
if _, err := ListSystemConfigsByKeys(ctx, []string{model.ConfigKeySiteName}); err != nil {
|
||||
t.Fatalf("ListSystemConfigsByKeys(site_name) error = %v", err)
|
||||
// Invalidate
|
||||
if err := InvalidateSystemConfigCache(ctx, model.ConfigKeySiteName); err != nil {
|
||||
t.Fatalf("InvalidateSystemConfigCache error = %v", err)
|
||||
}
|
||||
|
||||
cached, ok := systemConfigRAMCache.GetIfPresent(model.ConfigKeySiteName)
|
||||
if !ok {
|
||||
t.Fatal("expected RAM cache to be populated after redis hit")
|
||||
// Wait for broadcast execution
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Fetch again
|
||||
updated, err := GetSystemConfigByKey(ctx, model.ConfigKeySiteName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSystemConfigByKey error = %v", err)
|
||||
}
|
||||
if cached.Value != "Wavelet" {
|
||||
t.Fatalf("RAM cache value = %q, want %q", cached.Value, "Wavelet")
|
||||
if updated.Value != "new_site_name" {
|
||||
t.Fatalf("value = %q, want %q", updated.Value, "new_site_name")
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,12 @@ func GetUserByUsername(ctx context.Context, username string) (model.User, error)
|
||||
// GetSystemUser loads the built-in system user, or returns a synthetic fallback.
|
||||
func GetSystemUser(ctx context.Context) model.User {
|
||||
var user model.User
|
||||
if err := db.DB(ctx).Where("username = ?", "system").First(&user).Error; err == nil {
|
||||
if err := db.DB(ctx).Where("username = ?", configTypeSystem).First(&user).Error; err == nil {
|
||||
return user
|
||||
}
|
||||
return model.User{
|
||||
ID: 999,
|
||||
Username: "system",
|
||||
Username: configTypeSystem,
|
||||
Nickname: "系统",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user