mirror of
https://github.com/go-gost/x.git
synced 2026-10-20 18:32:36 +00:00
Implements the cache.Cache interface with in-memory backend (TTL, maxSize/maxBytes bounds, oldest/LRU eviction, background cleanup). Adds httpcache layer for HTTP response policy (per-status TTL, serve-stale, method/body filtering). Wires into config parser, loader, registry, and all four sniffing handlers (forward/local, forward/remote, sni, http) so HTTP responses are cached before dialing upstream. Bumps core dependency to v0.6.0 for the new cache.Cache interface.
242 lines
5.8 KiB
Go
242 lines
5.8 KiB
Go
// Package cache provides in-memory implementations of the core cache.Cache
|
|
// interface.
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/cache"
|
|
"github.com/go-gost/core/logger"
|
|
)
|
|
|
|
// EvictionPolicy selects which entry to remove when the cache is full.
|
|
type EvictionPolicy string
|
|
|
|
const (
|
|
// EvictOldest removes the entry inserted least recently (by insertion time).
|
|
EvictOldest EvictionPolicy = "oldest"
|
|
// EvictLRU removes the entry accessed least recently.
|
|
EvictLRU EvictionPolicy = "lru"
|
|
)
|
|
|
|
// Options configures a memory cache.
|
|
type Options struct {
|
|
// DefaultTTL is applied to entries stored without an explicit TTL.
|
|
// A non-positive value means entries never expire by default.
|
|
DefaultTTL time.Duration
|
|
// MaxSize bounds the number of entries. 0 means unbounded.
|
|
MaxSize int
|
|
// MaxBytes bounds the total size of stored data in bytes. 0 means unbounded.
|
|
MaxBytes int64
|
|
// CleanupInterval sets the period for the background expiry sweep.
|
|
// A non-positive value disables the background goroutine.
|
|
CleanupInterval time.Duration
|
|
// Eviction is the policy used when MaxSize or MaxBytes is exceeded.
|
|
// Defaults to EvictOldest.
|
|
Eviction EvictionPolicy
|
|
// Logger is used for debug logging. Optional.
|
|
Logger logger.Logger
|
|
}
|
|
|
|
type entry struct {
|
|
data []byte
|
|
expiration time.Time // zero = never expires
|
|
created time.Time
|
|
lastAccess time.Time
|
|
}
|
|
|
|
// memoryCache is a concurrency-safe in-memory cache.Cache implementation.
|
|
type memoryCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]*entry
|
|
currentBytes int64
|
|
|
|
defaultTTL time.Duration
|
|
maxSize int
|
|
maxBytes int64
|
|
eviction EvictionPolicy
|
|
logger logger.Logger
|
|
|
|
stopCleanup chan struct{}
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
// NewMemoryCache creates an in-memory cache. If opts.CleanupInterval is
|
|
// positive, a background goroutine periodically removes expired entries; it is
|
|
// stopped by Close.
|
|
func NewMemoryCache(opts Options) cache.Cache {
|
|
eviction := opts.Eviction
|
|
if eviction != EvictLRU {
|
|
eviction = EvictOldest
|
|
}
|
|
|
|
c := &memoryCache{
|
|
entries: make(map[string]*entry),
|
|
defaultTTL: opts.DefaultTTL,
|
|
maxSize: opts.MaxSize,
|
|
maxBytes: opts.MaxBytes,
|
|
eviction: eviction,
|
|
logger: opts.Logger,
|
|
stopCleanup: make(chan struct{}),
|
|
}
|
|
|
|
if opts.CleanupInterval > 0 {
|
|
go c.cleanupLoop(opts.CleanupInterval)
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// Get returns the entry for key. An expired-but-present entry is returned with
|
|
// cache.ErrNotFound reserved for absent keys, so callers can serve stale.
|
|
func (c *memoryCache) Get(ctx context.Context, key string) (*cache.Entry, error) {
|
|
c.mu.Lock()
|
|
e, ok := c.entries[key]
|
|
if ok {
|
|
e.lastAccess = time.Now()
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if !ok {
|
|
return nil, cache.ErrNotFound
|
|
}
|
|
|
|
// Return a copy so callers can't mutate the stored buffer.
|
|
data := make([]byte, len(e.data))
|
|
copy(data, e.data)
|
|
return &cache.Entry{Data: data, Expiration: e.expiration}, nil
|
|
}
|
|
|
|
// Set stores data under key. A non-positive TTL uses the cache default; a
|
|
// non-positive default means the entry never expires.
|
|
func (c *memoryCache) Set(ctx context.Context, key string, data []byte, opts ...cache.SetOption) error {
|
|
var so cache.SetOptions
|
|
for _, opt := range opts {
|
|
opt(&so)
|
|
}
|
|
|
|
ttl := so.TTL
|
|
if ttl <= 0 {
|
|
ttl = c.defaultTTL
|
|
}
|
|
var expiration time.Time
|
|
if ttl > 0 {
|
|
expiration = time.Now().Add(ttl)
|
|
}
|
|
|
|
cp := make([]byte, len(data))
|
|
copy(cp, data)
|
|
|
|
now := time.Now()
|
|
e := &entry{
|
|
data: cp,
|
|
expiration: expiration,
|
|
created: now,
|
|
lastAccess: now,
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Replace: account for the size delta of an existing key.
|
|
if old, ok := c.entries[key]; ok {
|
|
c.currentBytes -= int64(len(old.data))
|
|
}
|
|
c.entries[key] = e
|
|
c.currentBytes += int64(len(cp))
|
|
|
|
c.evictLocked()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete removes key from the cache.
|
|
func (c *memoryCache) Delete(ctx context.Context, key string) error {
|
|
c.mu.Lock()
|
|
if e, ok := c.entries[key]; ok {
|
|
c.currentBytes -= int64(len(e.data))
|
|
delete(c.entries, key)
|
|
}
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Close stops the background cleanup goroutine. Safe to call more than once.
|
|
// Implements io.Closer so the registry stops the goroutine on hot reload.
|
|
func (c *memoryCache) Close() error {
|
|
c.stopOnce.Do(func() { close(c.stopCleanup) })
|
|
return nil
|
|
}
|
|
|
|
func (c *memoryCache) cleanupLoop(interval time.Duration) {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-c.stopCleanup:
|
|
return
|
|
case <-ticker.C:
|
|
c.mu.Lock()
|
|
now := time.Now()
|
|
for k, e := range c.entries {
|
|
if !e.expiration.IsZero() && now.After(e.expiration) {
|
|
c.currentBytes -= int64(len(e.data))
|
|
delete(c.entries, k)
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
// evictLocked enforces the size/byte bounds. It first drops expired entries,
|
|
// then evicts by policy until within bounds. Caller must hold c.mu.
|
|
func (c *memoryCache) evictLocked() {
|
|
if !c.overCapacityLocked() {
|
|
return
|
|
}
|
|
|
|
// First pass: drop expired entries — they're free wins.
|
|
now := time.Now()
|
|
for k, e := range c.entries {
|
|
if !e.expiration.IsZero() && now.After(e.expiration) {
|
|
c.currentBytes -= int64(len(e.data))
|
|
delete(c.entries, k)
|
|
}
|
|
}
|
|
|
|
// Second pass: evict by policy until within bounds.
|
|
for c.overCapacityLocked() && len(c.entries) > 0 {
|
|
var victim string
|
|
var victimTS time.Time
|
|
first := true
|
|
for k, e := range c.entries {
|
|
ts := e.created
|
|
if c.eviction == EvictLRU {
|
|
ts = e.lastAccess
|
|
}
|
|
if first || ts.Before(victimTS) {
|
|
victim = k
|
|
victimTS = ts
|
|
first = false
|
|
}
|
|
}
|
|
if e, ok := c.entries[victim]; ok {
|
|
c.currentBytes -= int64(len(e.data))
|
|
delete(c.entries, victim)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *memoryCache) overCapacityLocked() bool {
|
|
if c.maxSize > 0 && len(c.entries) > c.maxSize {
|
|
return true
|
|
}
|
|
if c.maxBytes > 0 && c.currentBytes > c.maxBytes {
|
|
return true
|
|
}
|
|
return false
|
|
}
|