mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
perf(cache): 三层缓存框架补强
- 新增 cache-framework skill,规范 RAM→Redis→DB 读路径、失效与 pub/sub - 上传元数据 Otter+Redis 缓存与多节点失效;Auth Source 缓存与 pub/sub - ListSystemConfigsByKeys 补 Redis 层;上传统计单事务;登录/Token 缓存预热 - cleanup 任务补 upload meta 失效钩子
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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 setupAuthSourceCacheTest(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) {
|
||||
t.Helper()
|
||||
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open in-memory SQLite db: %v", err)
|
||||
}
|
||||
if err := sqliteDB.AutoMigrate(&model.AuthSource{}); err != nil {
|
||||
t.Fatalf("failed to migrate auth sources: %v", err)
|
||||
}
|
||||
|
||||
miniRedis, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start miniredis: %v", err)
|
||||
}
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
db.Redis = redis.NewClient(&redis.Options{
|
||||
Addr: miniRedis.Addr(),
|
||||
MaintNotificationsConfig: &maintnotifications.Config{
|
||||
Mode: maintnotifications.ModeDisabled,
|
||||
},
|
||||
})
|
||||
|
||||
ResetAuthSourceRAMCacheForTest()
|
||||
|
||||
cleanup := func() {
|
||||
StopAuthSourceCacheListener()
|
||||
ResetAuthSourceRAMCacheForTest()
|
||||
db.Redis.Close()
|
||||
miniRedis.Close()
|
||||
db.Redis = nil
|
||||
}
|
||||
return sqliteDB, miniRedis, cleanup
|
||||
}
|
||||
|
||||
func TestGetActiveAuthSourcesCached_LoadsFromRedisBeforeDB(t *testing.T) {
|
||||
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
||||
}
|
||||
|
||||
source := model.AuthSource{
|
||||
Name: "cached-source",
|
||||
Type: model.AuthSourceTypeOIDC,
|
||||
DisplayName: "Cached Source",
|
||||
IsActive: true,
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
OpenIDDiscoveryURL: "https://issuer.example.com",
|
||||
}
|
||||
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
||||
t.Fatalf("CreateAuthSource() error = %v", err)
|
||||
}
|
||||
|
||||
warmed, err := GetActiveAuthSourcesCached(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() warm error = %v", err)
|
||||
}
|
||||
if len(warmed) == 0 || warmed[0].Name != source.Name {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() warm = %#v, want source %q", warmed, source.Name)
|
||||
}
|
||||
|
||||
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
||||
t.Fatalf("Delete(auth source) error = %v", err)
|
||||
}
|
||||
|
||||
ResetAuthSourceRAMCacheForTest()
|
||||
|
||||
cached, err := GetActiveAuthSourcesCached(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() cached error = %v", err)
|
||||
}
|
||||
if len(cached) == 0 || cached[0].Name != source.Name {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() = %#v, want redis-backed source %q", cached, source.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthSourceByNameCached_LoadsFromRedisBeforeDB(t *testing.T) {
|
||||
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
||||
}
|
||||
|
||||
source := model.AuthSource{
|
||||
Name: "by-name-source",
|
||||
Type: model.AuthSourceTypeOIDC,
|
||||
DisplayName: "By Name Source",
|
||||
IsActive: true,
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
OpenIDDiscoveryURL: "https://issuer.example.com",
|
||||
}
|
||||
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
||||
t.Fatalf("CreateAuthSource() error = %v", err)
|
||||
}
|
||||
|
||||
warmed, err := GetAuthSourceByNameCached(ctx, source.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAuthSourceByNameCached() warm error = %v", err)
|
||||
}
|
||||
if warmed.Name != source.Name || warmed.ClientSecret != source.ClientSecret {
|
||||
t.Fatalf("GetAuthSourceByNameCached() warm = %#v, want %#v", warmed, source)
|
||||
}
|
||||
|
||||
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
||||
t.Fatalf("Delete(auth source) error = %v", err)
|
||||
}
|
||||
|
||||
ResetAuthSourceRAMCacheForTest()
|
||||
|
||||
cached, err := GetAuthSourceByNameCached(ctx, source.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAuthSourceByNameCached() cached error = %v", err)
|
||||
}
|
||||
if cached.Name != source.Name || cached.ClientSecret != source.ClientSecret {
|
||||
t.Fatalf("GetAuthSourceByNameCached() = %#v, want redis-backed source %#v", cached, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateAuthSourceCache_ClearsRedisKeys(t *testing.T) {
|
||||
_, _, cleanup := setupAuthSourceCacheTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAuthSourceCache() initial error = %v", err)
|
||||
}
|
||||
|
||||
source := model.AuthSource{
|
||||
Name: "invalidate-source",
|
||||
Type: model.AuthSourceTypeOIDC,
|
||||
DisplayName: "Invalidate Source",
|
||||
IsActive: true,
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
OpenIDDiscoveryURL: "https://issuer.example.com",
|
||||
}
|
||||
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
||||
t.Fatalf("CreateAuthSource() error = %v", err)
|
||||
}
|
||||
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() warm error = %v", err)
|
||||
}
|
||||
if _, err := GetAuthSourceByNameCached(ctx, source.Name); err != nil {
|
||||
t.Fatalf("GetAuthSourceByNameCached() warm error = %v", err)
|
||||
}
|
||||
|
||||
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
||||
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
||||
}
|
||||
|
||||
activeExists, err := db.Redis.Exists(ctx, db.PrefixedKey(authSourceActiveRedisKey)).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("Exists(active key) error = %v", err)
|
||||
}
|
||||
if activeExists != 0 {
|
||||
t.Fatalf("active redis key still exists after invalidation")
|
||||
}
|
||||
|
||||
byNameExists, err := db.Redis.Exists(ctx, db.PrefixedKey(authSourceByNameRedisKey(source.Name))).Result()
|
||||
if err != nil {
|
||||
t.Fatalf("Exists(by-name key) error = %v", err)
|
||||
}
|
||||
if byNameExists != 0 {
|
||||
t.Fatalf("by-name redis key still exists after invalidation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthSourceInvalidationPubSubClearsPeerRAM(t *testing.T) {
|
||||
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
source := model.AuthSource{
|
||||
Name: "pubsub-source",
|
||||
Type: model.AuthSourceTypeOIDC,
|
||||
DisplayName: "PubSub Source",
|
||||
IsActive: true,
|
||||
ClientID: "client-id",
|
||||
ClientSecret: "client-secret",
|
||||
OpenIDDiscoveryURL: "https://issuer.example.com",
|
||||
}
|
||||
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
||||
t.Fatalf("CreateAuthSource() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
||||
t.Fatalf("GetActiveAuthSourcesCached() error = %v", err)
|
||||
}
|
||||
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
||||
t.Fatalf("Delete(auth source) error = %v", err)
|
||||
}
|
||||
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
||||
t.Fatalf("expected RAM cache hit before pub/sub invalidation: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Redis.Publish(ctx, authSourceInvalidationChannel, "reset").Err(); err != nil {
|
||||
t.Fatalf("publish invalidation: %v", err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if _, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); !ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if _, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); ok {
|
||||
t.Fatal("expected peer RAM cache to be cleared by pub/sub")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user