lolly/internal/proxy/cache_handler.go
xfy cf2fcca7e8 refactor: 提取公共逻辑、消除重复代码、加强错误处理
- 提取 App 公共逻辑到 app_common.go,消除 app.go/app_windows.go 重复定义
- 提取 Server 生命周期/中间件/路由逻辑到独立文件(lifecycle.go/middleware_builder.go/router.go)
- 提取 Proxy 缓存处理/头部修改/目标选择到独立模块
- 提取 CheckIPAccess/CheckTokenAuth 到 utils/httperror.go,消除 status/purge 重复实现
- 修复 stream 双向转发:任一方向完成立即关闭双端,避免连接泄漏
- 修复 SSL/TLS 中静默忽略错误的问题,添加日志记录
- 统一日志消息为英文

💘 Generated with Crush

Assisted-by: GLM 5.1 via Crush <crush@charm.land>
2026-04-28 18:00:48 +08:00

198 lines
5.5 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 proxy
import (
"hash/fnv"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/cache"
"rua.plus/lolly/internal/loadbalance"
)
// buildCacheKey 构建缓存键字符串。
//
// 使用请求方法和完整请求 URI 作为缓存键。
// 该函数保留用于日志记录和调试场景。
//
// 参数:
// - ctx: FastHTTP 请求上下文
//
// 返回值:
// - string: 缓存键(格式 "METHOD:URI"
func (p *Proxy) buildCacheKey(ctx *fasthttp.RequestCtx) string {
// 使用请求方法和路径作为缓存键
return string(ctx.Request.Header.Method()) + ":" + string(ctx.Request.URI().RequestURI())
}
// buildCacheKeyHash 使用 FNV-64a 计算缓存键的 uint64 哈希值。
// 返回哈希值和原始字符串键。
// 注意:此函数会先构建字符串键再哈希,存在双重分配。
// 对于只需要哈希值的场景,使用 buildCacheKeyHashValue 代替。
func (p *Proxy) buildCacheKeyHash(ctx *fasthttp.RequestCtx) (uint64, string) {
// 构建原始 key
origKey := p.buildCacheKey(ctx)
// 使用 FNV-64a 计算哈希
h := fnv.New64a()
h.Write([]byte(origKey))
return h.Sum64(), origKey
}
// buildCacheKeyHashValue 直接计算缓存键的哈希值,零字符串分配。
// 用于只需要哈希值而不需要原始键的场景。
func (p *Proxy) buildCacheKeyHashValue(ctx *fasthttp.RequestCtx) uint64 {
h := fnv.New64a()
h.Write(ctx.Request.Header.Method())
h.Write([]byte(":"))
h.Write(ctx.Request.URI().RequestURI())
return h.Sum64()
}
// writeCachedResponse 将缓存的响应写入 FastHTTP 响应上下文。
//
// 设置响应体、状态码、响应头,并添加 X-Cache: HIT 头标记缓存命中。
//
// 参数:
// - ctx: FastHTTP 请求上下文
// - entry: 缓存条目,包含响应数据和元数据
func (p *Proxy) writeCachedResponse(ctx *fasthttp.RequestCtx, entry *cache.ProxyCacheEntry) {
ctx.Response.SetBody(entry.Data)
ctx.Response.SetStatusCode(entry.Status)
for key, value := range entry.Headers {
ctx.Response.Header.Set(key, value)
}
ctx.Response.Header.Set("X-Cache", "HIT")
}
// backgroundRefresh 在后台异步刷新缓存条目。
//
// 向对应的上游目标发送请求,获取最新响应并更新缓存。
// 该方法在独立 goroutine 中运行,不阻塞主请求流程。
//
// 参数:
// - ctx: 原始 FastHTTP 请求上下文(仅用于复制请求信息)
// - target: 要刷新的后端目标
// - hashKey: 缓存哈希键
// - origKey: 缓存原始键
func (p *Proxy) backgroundRefresh(ctx *fasthttp.RequestCtx, target *loadbalance.Target, hashKey uint64, origKey string) {
// 创建新的请求上下文副本
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
// 复制原始请求
ctx.Request.CopyTo(req)
// 如果启用 Revalidate添加条件请求头
if p.config.Cache.Revalidate {
if entry, ok, _ := p.cache.Get(hashKey, origKey); ok {
if entry.LastModified != "" {
req.Header.Set("If-Modified-Since", entry.LastModified)
}
if entry.ETag != "" {
req.Header.Set("If-None-Match", entry.ETag)
}
}
}
// 获取客户端
client := p.getClient(target.URL)
if client == nil {
return
}
// 执行请求
err := client.Do(req, resp)
if err != nil {
p.cache.ReleaseLock(hashKey, err)
return
}
// 处理 304 Not Modified 响应
if resp.StatusCode() == 304 {
newHeaders := make(map[string]string)
if lm := resp.Header.Peek("Last-Modified"); len(lm) > 0 {
newHeaders["Last-Modified"] = string(lm)
}
if et := resp.Header.Peek("ETag"); len(et) > 0 {
newHeaders["ETag"] = string(et)
}
p.cache.RefreshTTL(hashKey, origKey, newHeaders)
return
}
// 提取响应头(使用 pool 复用 map
headers, ok := headersPool.Get().(map[string]string)
if !ok {
headers = make(map[string]string, 20)
}
for k := range headers {
delete(headers, k)
}
for key, value := range resp.Header.All() {
headers[string(key)] = string(value)
}
// 更新缓存
p.cache.Set(hashKey, origKey, resp.Body(), headers, resp.StatusCode(), p.getCacheDuration(resp.StatusCode()))
}
// GetCache 返回代理的 ProxyCache 实例(用于 purge handler
// 如果缓存未启用,返回 nil。
func (p *Proxy) GetCache() *cache.ProxyCache {
return p.cache
}
// GetCacheStats 返回代理缓存的统计信息。
// 如果缓存未启用,返回 nil。
func (p *Proxy) GetCacheStats() *cache.ProxyCacheStats {
if p.cache == nil {
return nil
}
stats := p.cache.Stats()
return &stats
}
// getCacheDuration 根据状态码获取缓存时间。
// 优先级CacheValid 配置 > MaxAge
//
// 映射规则:
// - 200-299: CacheValid.OK0 时继承 MaxAge
// - 301/302: CacheValid.Redirect
// - 404: CacheValid.NotFound
// - 400-499除 404: CacheValid.ClientError
// - 500-599: CacheValid.ServerError
// - 其他: 不缓存(返回 0
func (p *Proxy) getCacheDuration(statusCode int) time.Duration {
// 无 CacheValid 配置,使用 MaxAge
if p.config.CacheValid == nil {
return p.config.Cache.MaxAge
}
cv := p.config.CacheValid
switch {
case statusCode >= 200 && statusCode < 300:
if cv.OK > 0 {
return cv.OK
}
return p.config.Cache.MaxAge // 0 表示继承 MaxAge
case statusCode == 301 || statusCode == 302:
return cv.Redirect // 0 表示不缓存
case statusCode == 404:
return cv.NotFound
case statusCode >= 400 && statusCode < 500:
return cv.ClientError
case statusCode >= 500:
return cv.ServerError
default:
return 0 // 不缓存
}
}