onesvm-browser-server/server/internal/gateway/cache.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

135 lines
3.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.

// 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
}