lolly/internal/cache/disk_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

387 lines
7.8 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 (
"os"
"path/filepath"
"testing"
"time"
)
func TestNewDiskCache(t *testing.T) {
// 创建临时目录
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
// 等待懒加载完成
<-dc.loadCh
if dc.basePath != tmpDir {
t.Errorf("basePath = %q, want %q", dc.basePath, tmpDir)
}
}
func TestDiskCacheSetGet(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
// 等待懒加载完成
<-dc.loadCh
// 设置缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("test response data")
headers := map[string]string{"Content-Type": "application/json"}
status := 200
maxAge := 10 * time.Minute
dc.Set(hashKey, origKey, data, headers, status, maxAge)
// 获取缓存
entry, exists, stale := dc.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)
}
if entry.Status != status {
t.Errorf("Status = %d, want %d", entry.Status, status)
}
if entry.Headers["Content-Type"] != "application/json" {
t.Errorf("Headers[Content-Type] = %q, want %q", entry.Headers["Content-Type"], "application/json")
}
}
func TestDiskCacheDelete(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 设置缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
dc.Set(hashKey, origKey, []byte("test"), nil, 200, 10*time.Minute)
// 验证存在
_, exists, _ := dc.Get(hashKey, origKey)
if !exists {
t.Fatal("entry should exist before delete")
}
// 删除
if err := dc.Delete(hashKey); err != nil {
t.Fatalf("Delete failed: %v", err)
}
// 验证已删除
_, exists, _ = dc.Get(hashKey, origKey)
if exists {
t.Error("entry should not exist after delete")
}
}
func TestDiskCacheStale(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 设置一个已过期的缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
dc.Set(hashKey, origKey, []byte("test"), nil, 200, 1*time.Millisecond)
// 等待过期
time.Sleep(10 * time.Millisecond)
// 获取缓存
entry, exists, stale := dc.Get(hashKey, origKey)
if !exists {
t.Fatal("expired entry should still exist")
}
if !stale {
t.Error("expired entry should be marked as stale")
}
if string(entry.Data) != "test" {
t.Errorf("Data = %q, want %q", entry.Data, "test")
}
}
func TestDiskCacheLevels(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 设置缓存
hashKey := uint64(0xabcdef1234567890)
origKey := "GET:/api/test"
dc.Set(hashKey, origKey, []byte("test"), nil, 200, 10*time.Minute)
// 验证文件路径包含层级目录
dataPath := dc.filePathFromHash(hashKey, "data")
if filepath.Dir(dataPath) == tmpDir {
t.Error("file should be in a subdirectory for levels=1:2")
}
// 验证文件存在
if _, err := os.Stat(dataPath); os.IsNotExist(err) {
t.Errorf("data file not found at %s", dataPath)
}
}
func TestDiskCacheMaxSize(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
MaxSize: 100, // 很小的限制
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 设置多个缓存条目
for i := range 10 {
hashKey := uint64(i)
origKey := "GET:/api/test"
dc.Set(hashKey, origKey, []byte("test data that is longer than 10 bytes"), nil, 200, 10*time.Minute)
}
// 等待淘汰完成(淘汰是异步的)
time.Sleep(500 * time.Millisecond)
// 验证淘汰发生Evictions > 0
stats := dc.CacheStats()
if stats.Evictions == 0 {
t.Error("Evictions should be > 0 when MaxSize is exceeded")
}
}
func TestDiskCacheStats(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 初始统计
stats := dc.CacheStats()
if stats.Entries != 0 {
t.Errorf("initial Entries = %d, want 0", stats.Entries)
}
// 设置缓存
dc.Set(1, "key1", []byte("data1"), nil, 200, 10*time.Minute)
dc.Set(2, "key2", []byte("data2"), nil, 200, 10*time.Minute)
stats = dc.CacheStats()
if stats.Entries != 2 {
t.Errorf("Entries = %d, want 2", stats.Entries)
}
// 获取缓存(命中)
dc.Get(1, "key1")
stats = dc.CacheStats()
if stats.HitCount != 1 {
t.Errorf("HitCount = %d, want 1", stats.HitCount)
}
// 获取不存在的缓存(未命中)
dc.Get(999, "nonexistent")
stats = dc.CacheStats()
if stats.MissCount != 1 {
t.Errorf("MissCount = %d, want 1", stats.MissCount)
}
}
func TestDiskCacheCRC32(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
<-dc.loadCh
// 设置缓存
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("test data for crc32")
dc.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
// 获取缓存验证 CRC32
entry, exists, _ := dc.Get(hashKey, origKey)
if !exists {
t.Fatal("entry not found")
}
if string(entry.Data) != string(data) {
t.Errorf("Data mismatch")
}
}
func TestParseLevels(t *testing.T) {
tests := []struct {
input string
expected []int
}{
{"", nil},
{"1", []int{1}},
{"1:2", []int{1, 2}},
{"2:2:2", []int{2, 2, 2}},
}
for _, tt := range tests {
result := parseLevels(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("parseLevels(%q) = %v, want %v", tt.input, result, tt.expected)
continue
}
for i, v := range result {
if v != tt.expected[i] {
t.Errorf("parseLevels(%q)[%d] = %d, want %d", tt.input, i, v, tt.expected[i])
}
}
}
}
func TestDiskCacheLazyLoad(t *testing.T) {
tmpDir := t.TempDir()
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
defer dc.Stop()
// 在懒加载完成前loaded 应该是 false
// 但由于加载很快,我们无法可靠测试这个状态
// 所以我们等待加载完成
<-dc.loadCh
if !dc.loaded.Load() {
t.Error("loaded should be true after lazyLoad completes")
}
}
func TestDiskCacheRestart(t *testing.T) {
tmpDir := t.TempDir()
// 第一个实例:写入数据
cfg := &DiskCacheConfig{
Path: tmpDir,
Levels: "1:2",
}
dc1, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache failed: %v", err)
}
<-dc1.loadCh
hashKey := uint64(12345)
origKey := "GET:/api/test"
data := []byte("persistent data")
dc1.Set(hashKey, origKey, data, nil, 200, 10*time.Minute)
dc1.Stop()
// 第二个实例:读取数据(模拟重启)
dc2, err := NewDiskCache(cfg)
if err != nil {
t.Fatalf("NewDiskCache (restart) failed: %v", err)
}
<-dc2.loadCh
defer dc2.Stop()
// 验证数据恢复
entry, exists, _ := dc2.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)
}
}