// cache.go:搜索短缓存(plan-final §2.4:消化上游 CAPTCHA 的设计结论)。 // 进程内 LRU,容量 50,TTL 300s;键 = region+query+参数 hash。 // 仅缓存 search 成功信封;read 不缓存(页面可能变化 + 体积大)。 package gateway import ( "crypto/sha256" "encoding/hex" "encoding/json" "sort" "sync" "time" "onesvm.com/onesvm/browser-server/internal/contract" ) // CacheTTL 缓存有效期(design 值 300s)。 const CacheTTL = 300 * time.Second // CacheCap LRU 容量(design 值 50)。 const CacheCap = 50 // cachedEntry 缓存条目:缓存完整信封字节 + 命中时改写 provenance.cached=true。 type cachedEntry struct { body []byte // 原始信封 JSON(cached=false 时的形状) expiresAt time.Time // LRU 双向链表需要的前后指针(用 map+slice 简化:访问时间排序) lastUsed time.Time key string } // SearchCache 搜索短缓存(并发安全)。 type SearchCache struct { mu sync.Mutex m map[string]*cachedEntry now func() time.Time hits int64 miss int64 } // NewSearchCache 构造。 func NewSearchCache() *SearchCache { return &SearchCache{m: map[string]*cachedEntry{}, now: time.Now} } // cacheKey 缓存键:region + query + 参数 hash(max_results/time_range/lang)。 func cacheKey(in *contract.SearchInput) string { h := sha256.New() h.Write([]byte(in.Region)) h.Write([]byte{0}) h.Write([]byte(in.Query)) h.Write([]byte{0}) if in.TimeRange != nil { h.Write([]byte(*in.TimeRange)) } h.Write([]byte{0}) if in.Lang != nil { h.Write([]byte(*in.Lang)) } h.Write([]byte{0}) h.Write([]byte{byte(in.MaxResults)}) return hex.EncodeToString(h.Sum(nil)) } // Get 命中返回改写后的信封字节(provenance.cached=true、usage.credits=0、 // provenance.retrieved_at 保留原值——数据确实是那时取的)。 func (c *SearchCache) Get(in *contract.SearchInput) ([]byte, bool) { c.mu.Lock() defer c.mu.Unlock() now := c.now() k := cacheKey(in) e, ok := c.m[k] if !ok { c.miss++ return nil, false } if now.After(e.expiresAt) { delete(c.m, k) c.miss++ return nil, false } e.lastUsed = now c.hits++ return rewriteCached(e.body, now), true } // rewriteCached 改写缓存命中信封:cached=true、credits=0。 // 通过结构化反序列化改字段再序列化,避免字符串替换脆弱性。 func rewriteCached(body []byte, now time.Time) []byte { var env contract.Envelope if err := json.Unmarshal(body, &env); err != nil { return body // 不应发生(写缓存的必是合法信封) } env.Provenance.Cached = true env.Usage.Credits = 0 env.TookMs = 0 // 缓存命中无执行耗时 _ = now b, err := json.Marshal(env) if err != nil { return body } return b } // Put 写缓存(仅 search 成功信封)。超容量按 lastUsed 淘汰最旧。 func (c *SearchCache) Put(in *contract.SearchInput, body []byte) { c.mu.Lock() defer c.mu.Unlock() now := c.now() k := cacheKey(in) c.m[k] = &cachedEntry{body: body, expiresAt: now.Add(CacheTTL), lastUsed: now, key: k} if len(c.m) > CacheCap { c.evictOldest() } } // evictOldest 淘汰最久未用条目(LRU)。 func (c *SearchCache) evictOldest() { keys := make([]string, 0, len(c.m)) for k := range c.m { keys = append(keys, k) } sort.Slice(keys, func(i, j int) bool { return c.m[keys[i]].lastUsed.Before(c.m[keys[j]].lastUsed) }) victim := keys[0] delete(c.m, victim) } // Stats 观测(/metrics 扩展)。 func (c *SearchCache) Stats() (hits, miss int64) { c.mu.Lock() defer c.mu.Unlock() return c.hits, c.miss }