From 40c6f2fcceb573502bf1953765104e5c56258b3a Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 26 Jan 2026 15:55:03 +0800 Subject: [PATCH] fix: align cache controls with config Remove unused head-check config, make TTL overrides explicit, and tighten revalidation to avoid stale cache behavior. --- configs/config.example.toml | 1 - configs/docker.sample.toml | 1 - configs/npm.sample.toml | 1 - internal/config/types.go | 26 +++++++++--------- internal/hubmodule/debian/hooks.go | 9 ++++++- internal/hubmodule/strategy.go | 9 ++++--- internal/proxy/handler.go | 27 ++++++++++++++----- internal/server/hub_registry_test.go | 9 +++---- specs/001-config-bootstrap/data-model.md | 1 - specs/002-fiber-single-proxy/data-model.md | 2 +- .../cache_strategy_override_test.go | 8 +++--- tests/integration/upstream_stub_test.go | 2 ++ 12 files changed, 58 insertions(+), 38 deletions(-) diff --git a/configs/config.example.toml b/configs/config.example.toml index 0fc7dfb..7f8808f 100644 --- a/configs/config.example.toml +++ b/configs/config.example.toml @@ -21,7 +21,6 @@ Type = "docker" Username = "" Password = "" CacheTTL = 43200 -EnableHeadCheck = true [[Hub]] Name = "composer-cache" diff --git a/configs/docker.sample.toml b/configs/docker.sample.toml index ae2ab91..96b14ea 100644 --- a/configs/docker.sample.toml +++ b/configs/docker.sample.toml @@ -19,4 +19,3 @@ Module = "legacy" Username = "" Password = "" CacheTTL = 43200 -EnableHeadCheck = true diff --git a/configs/npm.sample.toml b/configs/npm.sample.toml index cac5bd4..a9350f1 100644 --- a/configs/npm.sample.toml +++ b/configs/npm.sample.toml @@ -19,4 +19,3 @@ Module = "legacy" Username = "" Password = "" CacheTTL = 43200 -EnableHeadCheck = false diff --git a/internal/config/types.go b/internal/config/types.go index cb92d72..9790b63 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -64,16 +64,15 @@ type GlobalConfig struct { // HubConfig 决定单个代理实例如何与下游/上游交互。 type HubConfig struct { - Name string `mapstructure:"Name"` - Domain string `mapstructure:"Domain"` - Upstream string `mapstructure:"Upstream"` - Proxy string `mapstructure:"Proxy"` - Type string `mapstructure:"Type"` - Username string `mapstructure:"Username"` - Password string `mapstructure:"Password"` - CacheTTL Duration `mapstructure:"CacheTTL"` - ValidationMode string `mapstructure:"ValidationMode"` - EnableHeadCheck bool `mapstructure:"EnableHeadCheck"` + Name string `mapstructure:"Name"` + Domain string `mapstructure:"Domain"` + Upstream string `mapstructure:"Upstream"` + Proxy string `mapstructure:"Proxy"` + Type string `mapstructure:"Type"` + Username string `mapstructure:"Username"` + Password string `mapstructure:"Password"` + CacheTTL Duration `mapstructure:"CacheTTL"` + ValidationMode string `mapstructure:"ValidationMode"` } // Config 是 TOML 文件映射的整体结构。 @@ -109,11 +108,12 @@ func CredentialModes(hubs []HubConfig) []string { // StrategyOverrides 将 hub 层的 TTL/Validation 配置映射为模块策略覆盖项。 func (h HubConfig) StrategyOverrides(ttl time.Duration) hubmodule.StrategyOptions { - opts := hubmodule.StrategyOptions{ - TTLOverride: ttl, - } + opts := hubmodule.StrategyOptions{} if mode := strings.TrimSpace(h.ValidationMode); mode != "" { opts.ValidationOverride = hubmodule.ValidationMode(mode) } + if h.CacheTTL.DurationValue() > 0 { + opts.TTLOverride = ttl + } return opts } diff --git a/internal/hubmodule/debian/hooks.go b/internal/hubmodule/debian/hooks.go index 5eb9014..0fdb638 100644 --- a/internal/hubmodule/debian/hooks.go +++ b/internal/hubmodule/debian/hooks.go @@ -37,7 +37,14 @@ func cachePolicy(_ *hooks.RequestContext, locatorPath string, current hooks.Cach // 索引类(Release/Packages/Contents)需要 If-None-Match/If-Modified-Since 再验证。 if strings.HasSuffix(clean, "/release") || strings.HasSuffix(clean, "/inrelease") || - strings.HasSuffix(clean, "/release.gpg") { + strings.HasSuffix(clean, "/release.gpg") || + strings.HasSuffix(clean, "/packages") || + strings.HasSuffix(clean, "/packages.gz") || + strings.HasSuffix(clean, "/packages.xz") || + strings.HasSuffix(clean, "/sources") || + strings.HasSuffix(clean, "/sources.gz") || + strings.HasSuffix(clean, "/sources.xz") || + strings.Contains(clean, "/contents-") { current.AllowCache = true current.AllowStore = true current.RequireRevalidate = true diff --git a/internal/hubmodule/strategy.go b/internal/hubmodule/strategy.go index 0b545bf..8f37f71 100644 --- a/internal/hubmodule/strategy.go +++ b/internal/hubmodule/strategy.go @@ -11,13 +11,14 @@ type StrategyOptions struct { // ResolveStrategy 将模块的默认策略与 hub 级覆盖合并。 func ResolveStrategy(meta ModuleMetadata, opts StrategyOptions) CacheStrategyProfile { strategy := meta.CacheStrategy - if strategy.TTLHint > 0 && opts.TTLOverride > 0 { - strategy.TTLHint = opts.TTLOverride - } if opts.ValidationOverride != "" { strategy.ValidationMode = opts.ValidationOverride } - return normalizeStrategy(strategy) + strategy = normalizeStrategy(strategy) + if opts.TTLOverride > 0 { + strategy.TTLHint = opts.TTLOverride + } + return strategy } func normalizeStrategy(profile CacheStrategyProfile) CacheStrategyProfile { diff --git a/internal/proxy/handler.go b/internal/proxy/handler.go index 755e240..c6ad0b2 100644 --- a/internal/proxy/handler.go +++ b/internal/proxy/handler.go @@ -3,7 +3,7 @@ package proxy import ( "bytes" "context" - "crypto/sha1" + "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" @@ -202,10 +202,25 @@ func (h *Handler) serveCache( c.Status(status) if method == http.MethodHead { - // 对 HEAD 请求仍向上游发起一次 HEAD,以满足“显式请求 + 再验证”的期望。 if route != nil && route.UpstreamURL != nil { - if resp, err := h.revalidateRequest(c, route, resolveUpstreamURL(route, route.UpstreamURL, c, hook), result.Entry.Locator, ""); err == nil { - resp.Body.Close() + shouldRevalidate := true + if hook != nil && hook.hasHooks && hook.def.CachePolicy != nil { + policy := hook.def.CachePolicy(hook.ctx, result.Entry.Locator.Path, hooks.CachePolicy{ + AllowCache: true, + AllowStore: true, + RequireRevalidate: true, + }) + if !policy.AllowCache || !policy.AllowStore { + shouldRevalidate = false + } else { + shouldRevalidate = policy.RequireRevalidate + } + } + + if shouldRevalidate { + if resp, err := h.revalidateRequest(c, route, resolveUpstreamURL(route, route.UpstreamURL, c, hook), result.Entry.Locator, ""); err == nil { + resp.Body.Close() + } } } result.Reader.Close() @@ -571,7 +586,7 @@ func resolveContentType(route *server.HubRoute, locator cache.Locator, hook *hoo func buildLocator(route *server.HubRoute, c fiber.Ctx, clean string, rawQuery []byte) cache.Locator { query := rawQuery if len(query) > 0 { - sum := sha1.Sum(query) + sum := sha256.Sum256(query) clean = fmt.Sprintf("%s/__qs/%s", clean, hex.EncodeToString(sum[:])) } loc := cache.Locator{ @@ -791,7 +806,7 @@ func (h *Handler) isCacheFresh( return true, nil case http.StatusOK: if resp.Header.Get("Etag") == "" && resp.Header.Get("Docker-Content-Digest") == "" && resp.Header.Get("Last-Modified") == "" { - return true, nil + return false, nil } h.rememberETag(route, locator, resp) remote := extractModTime(resp.Header) diff --git a/internal/server/hub_registry_test.go b/internal/server/hub_registry_test.go index 264eb45..7aa41cd 100644 --- a/internal/server/hub_registry_test.go +++ b/internal/server/hub_registry_test.go @@ -15,11 +15,10 @@ func TestHubRegistryLookupByHost(t *testing.T) { }, Hubs: []config.HubConfig{ { - Name: "docker", - Domain: "docker.hub.local", - Type: "docker", - Upstream: "https://registry-1.docker.io", - EnableHeadCheck: true, + Name: "docker", + Domain: "docker.hub.local", + Type: "docker", + Upstream: "https://registry-1.docker.io", }, { Name: "npm", diff --git a/specs/001-config-bootstrap/data-model.md b/specs/001-config-bootstrap/data-model.md index c21cddf..56911b7 100644 --- a/specs/001-config-bootstrap/data-model.md +++ b/specs/001-config-bootstrap/data-model.md @@ -28,7 +28,6 @@ - `Upstream` (string, required, http/https URL) - `Proxy` (string, optional, URL) - `CacheTTL` (duration, optional, overrides global) - - `EnableHeadCheck` (bool, optional, default true) - **Validation Rules**: `Name` 必须唯一;`Domain` + `Port` 组合不得冲突;URL 必须可解析。 - **Relationships**: 属于 `Config`,在运行时用于初始化路由、缓存目录 `StoragePath/`。 diff --git a/specs/002-fiber-single-proxy/data-model.md b/specs/002-fiber-single-proxy/data-model.md index c0df1bb..9b09ada 100644 --- a/specs/002-fiber-single-proxy/data-model.md +++ b/specs/002-fiber-single-proxy/data-model.md @@ -4,7 +4,7 @@ ### HubRoute - **Description**: Host/端口到上游仓库的映射,供 Fiber 路由和 Proxy handler 使用。 -- **Fields**: `Name` (string, unique), `Domain` (string, FQDN), `Port` (int, 1-65535), `Upstream` (URL), `Proxy` (URL, optional), `CacheTTL` (duration override), `EnableHeadCheck` (bool). +- **Fields**: `Name` (string, unique), `Domain` (string, FQDN), `Port` (int, 1-65535), `Upstream` (URL), `Proxy` (URL, optional), `CacheTTL` (duration override). - **Validation**: Name 唯一;Domain 不含协议/路径;Upstream 必须 http/https。 - **Relationships**: 由 config 加载到 `HubRegistry`;与 CacheEntry、ProxyRequest 通过 `Name` 关联。 diff --git a/tests/integration/cache_strategy_override_test.go b/tests/integration/cache_strategy_override_test.go index a0cb8f6..4113662 100644 --- a/tests/integration/cache_strategy_override_test.go +++ b/tests/integration/cache_strategy_override_test.go @@ -68,8 +68,8 @@ func TestCacheStrategyOverrides(t *testing.T) { } resp2.Body.Close() - if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 0 { - t.Fatalf("expected no HEAD before TTL expiry, got %d", headCount) + if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 1 { + t.Fatalf("expected single HEAD before TTL expiry, got %d", headCount) } if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 { t.Fatalf("upstream should be hit once before TTL expiry, got %d", getCount) @@ -85,8 +85,8 @@ func TestCacheStrategyOverrides(t *testing.T) { } resp3.Body.Close() - if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 1 { - t.Fatalf("expected single HEAD after TTL expiry, got %d", headCount) + if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 2 { + t.Fatalf("expected two HEAD requests after TTL expiry, got %d", headCount) } if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 { t.Fatalf("upstream GET count should remain 1, got %d", getCount) diff --git a/tests/integration/upstream_stub_test.go b/tests/integration/upstream_stub_test.go index 34937f9..42b33ae 100644 --- a/tests/integration/upstream_stub_test.go +++ b/tests/integration/upstream_stub_test.go @@ -156,6 +156,7 @@ func registerDockerHandlers(mux *http.ServeMux, blob []byte) { func registerNPMHandlers(mux *http.ServeMux) { mux.HandleFunc("/lodash", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) resp := map[string]any{ "name": "lodash", "dist-tags": map[string]string{ @@ -174,6 +175,7 @@ func registerNPMHandlers(mux *http.ServeMux) { mux.HandleFunc("/lodash/-/lodash-4.17.21.tgz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) _, _ = w.Write([]byte("tarball-bytes")) }) }