/
codespawn
/
tgstream
Обзор
Документация
Войти
/
codespawn
/
tgstream
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
16
CI/CD
Аналитика
Безопасность
master
internal/adapter/memorycache/memorycache.go
140 строк
5 KB
codespawn
feat: first working version
27 июн 2026, 17:00
27 июн 2026, 17:00
492bb24
Код
Авторство
О чём код?
// Package memorycache реализует порт port.Cache[V] как L1-кэш приложения: // LRU + TTL + хранение протухших записей для stale-while-revalidate // (AGENTS.md §2). // // Ключевая деталь (AGENTS.md §2): при истечении TTL запись НЕ удаляется // немедленно, а помечается протухшей (fresh=false) и продолжает храниться до // вытеснения по LRU-размеру — чтобы её можно было отдать как stale fallback при // ошибке источника. Вытеснение — исключительно по recency (свежесть на него не // влияет): как свежие, так и протухшие записи живут по единому LRU-порядку. package memorycache import ( "container/list" "context" "log/slog" "sync" "time" "gitverse.ru/codespawn/tgstream/internal/domain" "gitverse.ru/codespawn/tgstream/internal/observe" "gitverse.ru/codespawn/tgstream/internal/port" ) // Config настраивает кэш. type Config struct { // Max — ёмкость в записях (число каналов, AGENTS.md §10 CACHE_SIZE). // Значение < 1 приводится к 1. Max int // Now — источник времени; nil → time.Now. Внедряется в тестах для // детерминированной проверки TTL (AGENTS.md §6). Now func() time.Time // Logger — логгер для Debug/Trace-трассировки L1 (hit/miss/stale, set, evict). // nil → slog.Default(). Кэш-операции видны только при повышенной детализации // (LOG_LEVEL=debug/trace) и позволяют реконструировать cache-aside без логов в ядре. Logger *slog.Logger } // Cache — LRU+TTL in-memory кэш. Реализует port.Cache[V]. type Cache[V any] struct { mu sync.Mutex now func() time.Time max int log *slog.Logger ll *list.List // front = наиболее свежий по recency items map[string]*list.Element // key → element(*entry) } // entry — содержимое узла LRU-списка. type entry[V any] struct { key string val V expires time.Time // now()+ttl на момент Set; now() >= expires → протухла } // New создаёт Cache заданной ёмкости. func New[V any](cfg Config) *Cache[V] { if cfg.Max < 1 { cfg.Max = 1 } if cfg.Now == nil { cfg.Now = time.Now } log := cfg.Logger if log == nil { log = slog.Default() } return &Cache[V]{ now: cfg.Now, max: cfg.Max, log: log, ll: list.New(), items: make(map[string]*list.Element), } } // Get возвращает запись и её статус свежести (AGENTS.md §2): // - ok=false — записи нет; // - ok, fresh — свежая (now < expires); // - ok, !fresh — протухшая, но сохранённая (stale fallback для SWR). // // Обращение (даже к протухшей записи) поднимает её в начало LRU. func (c *Cache[V]) Get(key string) (V, bool, bool) { c.mu.Lock() defer c.mu.Unlock() el, ok := c.items[key] if !ok { c.log.Log(context.Background(), observe.LevelTrace, "cache get miss", "key", key) var zero V return zero, false, false } c.ll.MoveToFront(el) e := el.Value.(*entry[V]) fresh := c.now().Before(e.expires) c.log.Log(context.Background(), observe.LevelTrace, "cache get hit", "key", key, "fresh", fresh) return e.val, fresh, true } // Set записывает (или обновляет) значение и (пере)устанавливает TTL. При // превышении ёмкости вытесняет наименее свежую по recency запись (LRU). func (c *Cache[V]) Set(key string, val V, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() if el, ok := c.items[key]; ok { e := el.Value.(*entry[V]) e.val = val e.expires = c.now().Add(ttl) c.ll.MoveToFront(el) c.log.Log(context.Background(), observe.LevelTrace, "cache set updated", "key", key, "ttl", ttl.String()) return } e := &entry[V]{key: key, val: val, expires: c.now().Add(ttl)} c.items[key] = c.ll.PushFront(e) c.log.Log(context.Background(), observe.LevelTrace, "cache set new", "key", key, "ttl", ttl.String(), "size", c.ll.Len()) for c.ll.Len() > c.max { c.evictBack() } } // evictBack удаляет наименее свежую по recency запись (хвост LRU). Вызывается // только при превышении ёмкости — протухшие записи не удаляются «по таймеру». func (c *Cache[V]) evictBack() { el := c.ll.Back() if el == nil { return } e := el.Value.(*entry[V]) c.ll.Remove(el) delete(c.items, e.key) c.log.Log(context.Background(), slog.LevelDebug, "cache evicted", "key", e.key) } // Гарантия реализации порта во время компиляции. var _ port.Cache[[]domain.Post] = (*Cache[[]domain.Post])(nil)