覆盖 ProxyCache、DiskCache、TieredCache 的 GetStale 方法, 测试场景包括:错误时可用、超时时可用、窗口过期不可用、未过期直接返回。 同步更新 NewProxyCache 调用签名。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
432 lines
8.9 KiB
Go
432 lines
8.9 KiB
Go
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)
|
||
}
|
||
}
|
||
|
||
func TestTieredCacheGetStaleL1Hit(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
cfg := &TieredCacheConfig{
|
||
L1MaxEntries: 100,
|
||
L1MaxSize: 1024 * 1024,
|
||
L2Config: &DiskCacheConfig{
|
||
Path: tmpDir,
|
||
Levels: "1:2",
|
||
},
|
||
StaleIfError: 200 * time.Millisecond,
|
||
StaleIfTimeout: 0,
|
||
}
|
||
|
||
tc, err := NewTieredCache(cfg)
|
||
if err != nil {
|
||
t.Fatalf("NewTieredCache failed: %v", err)
|
||
}
|
||
defer tc.Stop()
|
||
|
||
hashKey := uint64(54321)
|
||
origKey := "GET:/api/tiered"
|
||
tc.Set(hashKey, origKey, []byte("l1data"), nil, 200, 100*time.Millisecond)
|
||
|
||
// 等待过期但仍在 stale_if_error 窗口内
|
||
time.Sleep(150 * time.Millisecond)
|
||
|
||
// isTimeout=false,应该从 L1 获取 stale 缓存
|
||
entry, ok := tc.GetStale(hashKey, origKey, false)
|
||
if !ok {
|
||
t.Error("stale entry should be usable on error from L1")
|
||
}
|
||
if entry == nil || string(entry.Data) != "l1data" {
|
||
t.Errorf("entry.Data = %v, want %q", entry, "l1data")
|
||
}
|
||
|
||
// isTimeout=true,staleIfTimeout=0,不应该可用
|
||
if _, ok2 := tc.GetStale(hashKey, origKey, true); ok2 {
|
||
t.Error("stale entry should NOT be usable on timeout when staleIfTimeout=0")
|
||
}
|
||
}
|
||
|
||
func TestTieredCacheGetStaleMiss(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
cfg := &TieredCacheConfig{
|
||
L1MaxEntries: 100,
|
||
L1MaxSize: 1024 * 1024,
|
||
L2Config: &DiskCacheConfig{
|
||
Path: tmpDir,
|
||
Levels: "1:2",
|
||
},
|
||
StaleIfError: 200 * time.Millisecond,
|
||
StaleIfTimeout: 200 * time.Millisecond,
|
||
}
|
||
|
||
tc, err := NewTieredCache(cfg)
|
||
if err != nil {
|
||
t.Fatalf("NewTieredCache failed: %v", err)
|
||
}
|
||
defer tc.Stop()
|
||
|
||
// 不存在的 key
|
||
if _, ok := tc.GetStale(99999, "nonexistent", false); ok {
|
||
t.Error("should not find nonexistent key")
|
||
}
|
||
|
||
if _, ok2 := tc.GetStale(99999, "nonexistent", true); ok2 {
|
||
t.Error("should not find nonexistent key on timeout")
|
||
}
|
||
}
|