lolly/internal/cache/tiered_cache_test.go
xfy aae378433e feat(cache): 实现分层缓存架构
- 添加 CacheBackend 接口统一内存/磁盘缓存访问
- 实现 DiskCache 磁盘缓存后端,支持目录层级和原子写入
- 实现 TieredCache 分层缓存(L1 内存 + L2 磁盘)
- 修改 ProxyCache.Delete 返回 error 以符合接口
- 添加 CacheStats() 方法实现接口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 13:15:02 +08:00

360 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cache
import (
"testing"
"time"
)
func TestNewTieredCache(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
PromoteThreshold: 3,
PromoteInterval: 100 * time.Millisecond,
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
// 等待 L2 懒加载完成
<-tc.l2.loadCh
if tc.l1 == nil {
t.Error("l1 should not be nil")
}
if tc.l2 == nil {
t.Error("l2 should not be nil")
}
}
func TestTieredCacheSetGet(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 设置缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("test response data")
tc.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
// 等待 L2 异步写入完成
time.Sleep(50 * time.Millisecond)
// 获取缓存(应该从 L1 获取)
entry, exists, stale := tc.Get(hashKey, origKey)
if !exists {
t.Fatal("cache entry not found")
}
if stale {
t.Error("entry should not be stale")
}
if string(entry.Data) != string(data) {
t.Errorf("Data = %q, want %q", entry.Data, data)
}
// 验证 L1 命中
stats := tc.TieredCacheStats()
if stats.L1Hits != 1 {
t.Errorf("L1Hits = %d, want 1", stats.L1Hits)
}
}
func TestTieredCacheL2Fallback(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 直接写入 L2绕过 L1
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("test from l2")
tc.l2.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
// 获取缓存(应该从 L2 获取)
entry, exists, stale := tc.Get(hashKey, origKey)
if !exists {
t.Fatal("cache entry not found")
}
if stale {
t.Error("entry should not be stale")
}
if string(entry.Data) != string(data) {
t.Errorf("Data = %q, want %q", entry.Data, data)
}
// 验证 L2 命中
stats := tc.TieredCacheStats()
if stats.L2Hits != 1 {
t.Errorf("L2Hits = %d, want 1", stats.L2Hits)
}
}
func TestTieredCacheDelete(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 设置缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
tc.Set(hashKey, origKey, []byte("test"), nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
// 删除
if err := tc.Delete(hashKey); err != nil {
t.Fatalf("Delete failed: %v", err)
}
// 验证 L1 和 L2 都已删除
_, exists, _ := tc.l1.Get(hashKey, origKey)
if exists {
t.Error("entry should not exist in L1 after delete")
}
_, exists, _ = tc.l2.Get(hashKey, origKey)
if exists {
t.Error("entry should not exist in L2 after delete")
}
}
func TestTieredCacheStale(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 设置一个已过期的缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
tc.l2.Set(hashKey, origKey, []byte("test"), nil, 200, 1*time.Millisecond)
// 等待过期
time.Sleep(10 * time.Millisecond)
// 获取缓存
_, exists, stale := tc.Get(hashKey, origKey)
if !exists {
t.Fatal("expired entry should still exist")
}
if !stale {
t.Error("expired entry should be marked as stale")
}
}
func TestTieredCachePromote(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
PromoteThreshold: 2,
PromoteInterval: 50 * time.Millisecond,
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 直接写入 L2
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("test data")
tc.l2.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
// 访问两次(达到阈值)
tc.Get(hashKey, origKey)
tc.Get(hashKey, origKey)
// 等待提升检查
time.Sleep(100 * time.Millisecond)
// 验证提升发生
stats := tc.TieredCacheStats()
if stats.Promotes == 0 {
t.Error("promotes should be > 0 after reaching threshold")
}
}
func TestTieredCacheStats(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 设置缓存
tc.Set(1, "key1", []byte("data1"), nil, 200, 10*time.Minute)
tc.Set(2, "key2", []byte("data2"), nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
// 获取缓存
tc.Get(1, "key1") // L1 命中
tc.Get(1, "key1") // L1 命中
tc.Get(999, "nonexistent") // 未命中
stats := tc.TieredCacheStats()
if stats.L1Hits != 2 {
t.Errorf("L1Hits = %d, want 2", stats.L1Hits)
}
if stats.Misses != 1 {
t.Errorf("Misses = %d, want 1", stats.Misses)
}
}
func TestTieredCacheRestart(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
// 第一个实例:写入数据
tc1, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
<-tc1.l2.loadCh
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("persistent data")
tc1.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
tc1.Stop()
// 第二个实例:读取数据(模拟重启)
tc2, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache (restart) failed: %v", err)
}
<-tc2.l2.loadCh
defer tc2.Stop()
// 验证数据从 L2 恢复
entry, exists, _ := tc2.Get(hashKey, origKey)
if !exists {
t.Fatal("entry should exist after restart")
}
if string(entry.Data) != string(data) {
t.Errorf("Data = %q, want %q", entry.Data, data)
}
// 验证是从 L2 获取的
stats := tc2.TieredCacheStats()
if stats.L2Hits != 1 {
t.Errorf("L2Hits = %d, want 1", stats.L2Hits)
}
}
func TestTieredCacheCacheStats(t *testing.T) {
tmpDir := t.TempDir()
cfg := &TieredCacheConfig{
L2Config: &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
},
}
tc, err := NewTieredCache(cfg)
if err != nil {
t.Fatalf("NewTieredCache failed: %v", err)
}
defer tc.Stop()
<-tc.l2.loadCh
// 设置缓存
tc.Set(1, "key1", []byte("data1"), nil, 200, 10*time.Minute)
time.Sleep(50 * time.Millisecond)
// 获取缓存统计
stats := tc.CacheStats()
if stats.Entries < 1 {
t.Errorf("Entries = %d, should be >= 1", stats.Entries)
}
}