refactor(cache): 提取 MatchRule 辅助函数并扩展测试

将 MatchRule 拆分为 matchPath/matchMethod/matchStatus 三个辅助函数,
提升代码可读性和可测试性。新增测试覆盖:
- AcquireLockWithTimeout 锁超时机制
- RefreshTTL TTL 刷新功能
- SetValidationHeaders 验证头设置
- MatchRule 路径匹配变体(前缀/通配符/精确)
- min_uses 计数阈值

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-22 08:08:37 +08:00
parent 0a7f7170d5
commit d933c1bd98
2 changed files with 173 additions and 27 deletions

View File

@ -88,6 +88,130 @@ func TestFileCacheLRUEviction(t *testing.T) {
}
}
func TestAcquireLockWithTimeout(t *testing.T) {
pc := NewProxyCache(nil, true, 0)
key := hashKey("timeout-test")
// 测试获取锁
waitCh, timedOut := pc.AcquireLockWithTimeout(key, 100*time.Millisecond)
if waitCh != nil || timedOut {
t.Error("Expected to acquire lock immediately")
}
// 测试等待超时
done := make(chan struct{})
go func() {
time.Sleep(200 * time.Millisecond)
pc.ReleaseLock(key, nil)
close(done)
}()
_, timedOut = pc.AcquireLockWithTimeout(key, 50*time.Millisecond)
if !timedOut {
t.Error("Expected timeout when waiting for lock")
}
<-done
}
func TestRefreshTTL(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
key := hashKey("refresh-test")
origKey := "refresh-test"
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
newHeaders := map[string]string{
"Last-Modified": "Wed, 21 Oct 2015 07:28:00 GMT",
"ETag": "\"abc123\"",
}
ok := pc.RefreshTTL(key, origKey, newHeaders)
if !ok {
t.Error("Expected RefreshTTL to succeed")
}
entry, _, _ := pc.Get(key, origKey)
if entry.LastModified != newHeaders["Last-Modified"] {
t.Errorf("Expected Last-Modified to be updated")
}
if entry.ETag != newHeaders["ETag"] {
t.Errorf("Expected ETag to be updated")
}
}
func TestSetValidationHeaders(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
key := hashKey("validation-test")
origKey := "validation-test"
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
ok := pc.SetValidationHeaders(key, origKey, "Mon, 01 Jan 2024 00:00:00 GMT", "\"xyz789\"")
if !ok {
t.Error("Expected SetValidationHeaders to succeed")
}
entry, _, _ := pc.Get(key, origKey)
if entry.LastModified != "Mon, 01 Jan 2024 00:00:00 GMT" {
t.Errorf("Expected LastModified to be set")
}
if entry.ETag != "\"xyz789\"" {
t.Errorf("Expected ETag to be set")
}
}
func TestMatchRulePathVariants(t *testing.T) {
tests := []struct {
name string
rulePath string
reqPath string
want bool
}{
{"prefix_match", "/api/", "/api/users", true},
{"prefix_no_match", "/api/", "/other", false},
{"wildcard_match", "/static/*", "/static/css/style.css", true},
{"wildcard_no_match", "/static/*", "/api/users", false},
{"exact_match", "/api", "/api", true},
{"prefix_with_slash", "/api", "/api/users", true},
{"prefix_with_query", "/api", "/api?query=value", true},
{"no_match_similar", "/api", "/apiother", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rules := []ProxyCacheRule{
{Path: tt.rulePath, Methods: []string{"GET"}, MaxAge: time.Minute},
}
pc := NewProxyCache(rules, false, 0)
rule := pc.MatchRule(tt.reqPath, "GET", 0)
if (rule != nil) != tt.want {
t.Errorf("MatchRule(%s, %s) = %v, want %v", tt.rulePath, tt.reqPath, rule != nil, tt.want)
}
})
}
}
func TestMinUsesThreshold(t *testing.T) {
pc := NewProxyCache(nil, false, 0)
key := hashKey("minuses-test")
origKey := "minuses-test"
// 首次设置
pc.Set(key, origKey, []byte("data"), nil, 200, 10*time.Minute)
// 首次 GetUses = 1
entry, _, _ := pc.Get(key, origKey)
if entry.Uses.Load() != 1 {
t.Errorf("Expected Uses=1 after first Get, got %d", entry.Uses.Load())
}
// 第二次 GetUses = 2
pc.Get(key, origKey)
if entry.Uses.Load() != 2 {
t.Errorf("Expected Uses=2 after second Get, got %d", entry.Uses.Load())
}
}
func TestFileCacheSizeEviction(t *testing.T) {
// 最大 10 字节
fc := NewFileCache(0, 10, 1*time.Hour)

View File

@ -493,44 +493,66 @@ func (c *ProxyCache) ReleaseLock(hashKey uint64, err error) {
}
// MatchRule 检查请求是否匹配缓存规则。
// 路径匹配规则:
// - 以 "/" 结尾:前缀匹配(如 "/api/" 匹配 "/api/users"
// - 包含 "*":通配符匹配(如 "/static/*" 匹配 "/static/css/style.css"
// - 其他:精确匹配或带分隔符的前缀匹配(如 "/api" 匹配 "/api"、"/api/users"、"/api?query"
func (c *ProxyCache) MatchRule(path, method string, status int) *ProxyCacheRule {
for _, rule := range c.rules {
// 检查路径匹配
if rule.Path != "" {
// 如果路径以 / 结尾,使用前缀匹配
// 如果路径包含 *,使用通配符匹配
// 否则使用前缀匹配(允许 /api 匹配 /api/users
if strings.HasSuffix(rule.Path, "/") {
if !strings.HasPrefix(path, rule.Path) {
continue
}
} else if strings.Contains(rule.Path, "*") {
if !MatchPattern(rule.Path, path) {
continue
}
} else {
// 精确匹配或前缀匹配
if path != rule.Path && !strings.HasPrefix(path, rule.Path+"/") && !strings.HasPrefix(path, rule.Path+"?") && len(path) <= len(rule.Path) {
continue
}
}
}
// 检查方法
if len(rule.Methods) > 0 && !slices.Contains(rule.Methods, method) {
if !c.matchPath(rule.Path, path) {
continue
}
// 检查状态码
if len(rule.Statuses) > 0 && !slices.Contains(rule.Statuses, status) {
if !c.matchMethod(rule.Methods, method) {
continue
}
if !c.matchStatus(rule.Statuses, status) {
continue
}
return &rule
}
return nil
}
// matchPath 检查路径是否匹配规则路径。
func (c *ProxyCache) matchPath(rulePath, path string) bool {
if rulePath == "" {
return true
}
switch {
case strings.HasSuffix(rulePath, "/"):
// 前缀匹配:"/api/" 匹配 "/api/users"
return strings.HasPrefix(path, rulePath)
case strings.Contains(rulePath, "*"):
// 通配符匹配:"/static/*" 匹配 "/static/css/style.css"
return MatchPattern(rulePath, path)
default:
// 精确匹配或带分隔符的前缀匹配
// "/api" 匹配 "/api"、"/api/users"、"/api?query"
// 但不匹配 "/apiother"
if path == rulePath {
return true
}
return strings.HasPrefix(path, rulePath+"/") || strings.HasPrefix(path, rulePath+"?")
}
}
// matchMethod 检查方法是否在允许列表中。
func (c *ProxyCache) matchMethod(methods []string, method string) bool {
if len(methods) == 0 {
return true
}
return slices.Contains(methods, method)
}
// matchStatus 检查状态码是否在允许列表中。
func (c *ProxyCache) matchStatus(statuses []int, status int) bool {
if len(statuses) == 0 {
return true
}
return slices.Contains(statuses, status)
}
// Delete 删除缓存条目。
func (c *ProxyCache) Delete(hashKey uint64) {
c.mu.Lock()