项目文件夹

文件
杭州明婳科技 96ce883d41 feat: cron/storage/cache/ratelimit 实例化 + trace 纳入 App 生命周期 (v1.4.0)
完成 instance+facade 一致性收尾,闭合三个原始问题:
- cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + SwapDefault* 提升全局默认
  + 包级 facade 代理,照 database.Manager/jwt.Manager 模式),消除单进程多 App 互相污染。
- trace 纳入 App 生命周期(WithTrace + doInit trace.Init + doShutdown trace.Close +
  config.TraceConfig + 自动装 Middleware)。不做实例隔离(OTel 进程全局)。
- 删除 WithoutWire 空函数技术债。

附带修复:
- cache/ratelimit redis 注入(jwt 模型:注入优先、database.GetRedis() 兜底)。
- commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot
  (多 App 下 previous 可能是另一 App 的活跃资源,关闭致 redis: client is closed)。
- app.RateLimitRegistry() + App-bound 限流中间件(多 App per-App 计数隔离)。
- StorageManager.Close() 闭合 Init/Close 对称性(io.Closer 断言,预留扩展点),
  closeResources 与 rollbackReplacedResources 对称收口。

新增公开 API:WithTrace / WithCronTask;app.Scheduler()/Cache()/RedisClient()/
RateLimitRegistry();cron/storage/cache/middleware 各 SwapDefault*;config.TraceConfig;
middleware.RateLimitRegistry + App-bound 方法;middleware.WithRedisClient;
storage.StorageManager.Close。

Breaking:删 WithoutWire;cron.AddTask pre-Init 注册失效(改用 WithCronTask);
middleware.StopRateLimiters 语义收窄;commitReplacedResources 不再关 previous;
cache.NewRedisCacheWithRedis / CacheManager.InitWithRedis 等签名变更。
详见 CHANGELOG [1.4.0] 升级说明。

验证:go build + go vet + go test -race ./... 全绿(-buildvcs=false)。
独立对抗性复审无 CRITICAL/HIGH。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:25:37 +08:00

70 行
2.3 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package cache
import (
"testing"
"github.com/EthanCodeCraft/xlgo-core/database"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
// TestRedisCacheInjectedClientPriority 固化 Phase 4 核心契约(jwt 模型):
// redisCache.client() 注入优先、全局兜底。注入的 client 即使与 database.GetRedis()
// 不同也必须用注入的--这是多 App 缓存隔离的基础。
func TestRedisCacheInjectedClientPriority(t *testing.T) {
// 全局 redis = clientG
mrG := miniredis.RunT(t)
clientG := redis.NewClient(&redis.Options{Addr: mrG.Addr()})
t.Cleanup(func() { _ = clientG.Close() })
origGlobal := database.SetTestRedisClient(clientG)
t.Cleanup(func() { database.SetTestRedisClient(origGlobal) })
// 注入 clientA(与全局 clientG 不同实例)
mrA := miniredis.RunT(t)
clientA := redis.NewClient(&redis.Options{Addr: mrA.Addr()})
t.Cleanup(func() { _ = clientA.Close() })
// 注入优先
injected := &redisCache{injectedClient: clientA}
if got := injected.client(); got != clientA {
t.Fatalf("injected redisCache.client() = %v, want clientA (注入优先)", got)
}
// 未注入 -> 回退全局 clientG
fallback := &redisCache{}
if got := fallback.client(); got != clientG {
t.Fatalf("fallback redisCache.client() = %v, want clientG (全局兜底)", got)
}
// NewRedisCacheWithRedis 构造的实例持注入 client
svc := NewRedisCacheWithRedis(clientA)
rc, ok := svc.(*redisCache)
if !ok {
t.Fatalf("NewRedisCacheWithRedis returned %T, want *redisCache", svc)
}
if rc.client() != clientA {
t.Fatalf("NewRedisCacheWithRedis client = %v, want clientA", rc.client())
}
}
// TestSwapDefaultCacheManagerReturnsOld 固化 Phase 4Swap 返回被替换的旧 Manager 且不关闭。
func TestSwapDefaultCacheManagerReturnsOld(t *testing.T) {
orig := defaultCachePtr.Load()
defer SwapDefaultCacheManager(orig)
first := NewCacheManager()
if prev := SwapDefaultCacheManager(first); prev != orig {
t.Fatalf("Swap returned %v, want orig", prev)
}
second := NewCacheManager()
if returned := SwapDefaultCacheManager(second); returned != first {
t.Fatalf("Swap returned %v, want first", returned)
}
if defaultCachePtr.Load() != second {
t.Fatal("Swap did not install second as default")
}
if got := SwapDefaultCacheManager(nil); got != second {
t.Fatalf("Swap(nil) = %v, want second (current default)", got)
}
}