初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
Vendored
+129
@@ -0,0 +1,129 @@
|
||||
// Package cache 提供进程内 TTL 缓存与 singleflight 合并,服务 OCI 读放大
|
||||
// 与审计原始事件暂存等场景;单进程部署,无跨进程共享需求(调研①方案 A)。
|
||||
package cache
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
val any
|
||||
exp time.Time
|
||||
}
|
||||
|
||||
// flight 是一次进行中的回源;后到的同 key 调用等待它完成。
|
||||
type flight struct {
|
||||
wg sync.WaitGroup
|
||||
val any
|
||||
err error
|
||||
}
|
||||
|
||||
// Cache 是带键数上限的进程内 TTL 缓存;并发 miss 同 key 只回源一次。
|
||||
type Cache struct {
|
||||
mu sync.Mutex
|
||||
items map[string]entry
|
||||
flights map[string]*flight
|
||||
max int
|
||||
}
|
||||
|
||||
// New 创建缓存;max 为键数上限,到限时先清过期项、仍满则任意驱逐。
|
||||
func New(max int) *Cache {
|
||||
return &Cache{items: map[string]entry{}, flights: map[string]*flight{}, max: max}
|
||||
}
|
||||
|
||||
// Get 返回未过期的缓存值。
|
||||
func (c *Cache) Get(key string) (any, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
e, ok := c.items[key]
|
||||
if !ok || time.Now().After(e.exp) {
|
||||
return nil, false
|
||||
}
|
||||
return e.val, true
|
||||
}
|
||||
|
||||
// Set 写入缓存;TTL 附加至多 1/10 的随机抖动,避免整批同时过期。
|
||||
func (c *Cache) Set(key string, val any, ttl time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.items) >= c.max {
|
||||
c.evictLocked()
|
||||
}
|
||||
jitter := time.Duration(rand.Int64N(int64(ttl)/10 + 1))
|
||||
c.items[key] = entry{val: val, exp: time.Now().Add(ttl + jitter)}
|
||||
}
|
||||
|
||||
// evictLocked 先清全部过期项,仍到上限则任意删除腾出空位。
|
||||
func (c *Cache) evictLocked() {
|
||||
now := time.Now()
|
||||
for k, e := range c.items {
|
||||
if now.After(e.exp) {
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
for k := range c.items {
|
||||
if len(c.items) < c.max {
|
||||
return
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
|
||||
// DeletePrefix 删除全部以 prefix 开头的键(写操作后按租户前缀失效)。
|
||||
func (c *Cache) DeletePrefix(prefix string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for k := range c.items {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
delete(c.items, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// join 登记或加入 key 的进行中回源;返回 true 表示已有同 key 回源在跑。
|
||||
func (c *Cache) join(key string) (*flight, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if f, ok := c.flights[key]; ok {
|
||||
return f, true
|
||||
}
|
||||
f := &flight{}
|
||||
f.wg.Add(1)
|
||||
c.flights[key] = f
|
||||
return f, false
|
||||
}
|
||||
|
||||
// settle 结束回源登记并唤醒等待者。
|
||||
func (c *Cache) settle(key string, f *flight) {
|
||||
c.mu.Lock()
|
||||
delete(c.flights, key)
|
||||
c.mu.Unlock()
|
||||
f.wg.Done()
|
||||
}
|
||||
|
||||
// Do 返回缓存值;miss 时经 singleflight 回源,成功写缓存,错误不缓存
|
||||
// (等待者共享同一错误,下次调用重新回源)。
|
||||
func Do[T any](c *Cache, key string, ttl time.Duration, fn func() (T, error)) (T, error) {
|
||||
var zero T
|
||||
if v, ok := c.Get(key); ok {
|
||||
return v.(T), nil
|
||||
}
|
||||
f, joined := c.join(key)
|
||||
if joined {
|
||||
f.wg.Wait()
|
||||
if f.err != nil {
|
||||
return zero, f.err
|
||||
}
|
||||
return f.val.(T), nil
|
||||
}
|
||||
f.val, f.err = fn()
|
||||
c.settle(key, f)
|
||||
if f.err != nil {
|
||||
return zero, f.err
|
||||
}
|
||||
c.Set(key, f.val, ttl)
|
||||
return f.val.(T), nil
|
||||
}
|
||||
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetSetExpiry(t *testing.T) {
|
||||
c := New(10)
|
||||
if _, ok := c.Get("k"); ok {
|
||||
t.Fatal("空缓存不应命中")
|
||||
}
|
||||
c.Set("k", "v", 50*time.Millisecond)
|
||||
if v, ok := c.Get("k"); !ok || v.(string) != "v" {
|
||||
t.Fatalf("Get = %v %v, want v true", v, ok)
|
||||
}
|
||||
// jitter ≤ TTL/10,60ms 覆盖 50+5ms 上限
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
if _, ok := c.Get("k"); ok {
|
||||
t.Error("过期后不应命中")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePrefix(t *testing.T) {
|
||||
c := New(10)
|
||||
c.Set("t1|a", 1, time.Minute)
|
||||
c.Set("t1|b", 2, time.Minute)
|
||||
c.Set("t2|a", 3, time.Minute)
|
||||
c.DeletePrefix("t1|")
|
||||
if _, ok := c.Get("t1|a"); ok {
|
||||
t.Error("t1|a 应被前缀删除")
|
||||
}
|
||||
if _, ok := c.Get("t2|a"); !ok {
|
||||
t.Error("t2|a 不应被删除")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictAtMax(t *testing.T) {
|
||||
c := New(2)
|
||||
c.Set("a", 1, time.Minute)
|
||||
c.Set("b", 2, time.Minute)
|
||||
c.Set("c", 3, time.Minute)
|
||||
if n := len(c.items); n > 2 {
|
||||
t.Errorf("键数 = %d, 应不超过上限 2", n)
|
||||
}
|
||||
if _, ok := c.Get("c"); !ok {
|
||||
t.Error("最新写入的键应存在")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoSingleflight(t *testing.T) {
|
||||
c := New(10)
|
||||
var calls atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
v, err := Do(c, "k", time.Minute, func() (int, error) {
|
||||
calls.Add(1)
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
return 42, nil
|
||||
})
|
||||
if err != nil || v != 42 {
|
||||
t.Errorf("Do = %d, %v", v, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if n := calls.Load(); n != 1 {
|
||||
t.Errorf("并发 miss 回源 %d 次, want 1", n)
|
||||
}
|
||||
// 命中缓存不再回源
|
||||
if _, err := Do(c, "k", time.Minute, func() (int, error) { calls.Add(1); return 0, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := calls.Load(); n != 1 {
|
||||
t.Errorf("命中后回源 %d 次, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoErrorNotCached(t *testing.T) {
|
||||
c := New(10)
|
||||
boom := errors.New("boom")
|
||||
if _, err := Do(c, "k", time.Minute, func() (int, error) { return 0, boom }); !errors.Is(err, boom) {
|
||||
t.Fatalf("err = %v, want boom", err)
|
||||
}
|
||||
v, err := Do(c, "k", time.Minute, func() (int, error) { return 7, nil })
|
||||
if err != nil || v != 7 {
|
||||
t.Errorf("错误不应入缓存,重试 = %d, %v", v, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user