Files
elowdb-go/pkg/linedb/cache.go

291 lines
6.4 KiB
Go

package linedb
import (
"strings"
"sync"
"time"
)
// CacheEntry представляет запись в кэше
type CacheEntry struct {
Data any
Timestamp time.Time
TTL time.Duration
}
// RecordCache представляет кэш записей
type RecordCache struct {
cache map[string]*CacheEntry
mutex sync.RWMutex
maxSize int
ttl time.Duration
stopChan chan struct{}
}
// NewRecordCache создает новый кэш
func NewRecordCache(maxSize int, ttl time.Duration) *RecordCache {
cache := &RecordCache{
cache: make(map[string]*CacheEntry),
maxSize: maxSize,
ttl: ttl,
stopChan: make(chan struct{}),
}
// Запускаем очистку устаревших записей только если TTL > 0
if ttl > 0 {
go cache.cleanupLoop()
}
return cache
}
// Set устанавливает значение в кэш
func (c *RecordCache) Set(key string, value any) {
c.mutex.Lock()
defer c.mutex.Unlock()
// Проверяем размер кэша
if len(c.cache) >= c.maxSize {
c.evictOldest()
}
c.cache[key] = &CacheEntry{
Data: value,
Timestamp: time.Now(),
TTL: c.ttl,
}
}
// Get получает значение из кэша
func (c *RecordCache) Get(key string) (any, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
entry, exists := c.cache[key]
if !exists {
return nil, false
}
// Проверяем TTL
if c.ttl > 0 && time.Since(entry.Timestamp) > c.ttl {
delete(c.cache, key)
return nil, false
}
return entry.Data, true
}
// Has проверяет наличие ключа в кэше
func (c *RecordCache) Has(key string) bool {
_, exists := c.Get(key)
return exists
}
// Delete удаляет ключ из кэша
func (c *RecordCache) Delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.cache, key)
}
// Clear очищает кэш полностью.
func (c *RecordCache) Clear() {
c.mutex.Lock()
defer c.mutex.Unlock()
c.cache = make(map[string]*CacheEntry)
}
// ClearCollection удаляет записи кэша, относящиеся к коллекции collectionName:
// ключи ReadByFilter ("collection:filter" и "collection_part:filter") и SetByRecord ("id:collection", "id:collection_part").
func (c *RecordCache) ClearCollection(collectionName string) {
if collectionName == "" {
c.Clear()
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
for key := range c.cache {
if cacheKeyBelongsToCollection(key, collectionName) {
delete(c.cache, key)
}
}
}
func cacheKeyBelongsToCollection(key, collectionName string) bool {
if strings.HasPrefix(key, collectionName+":") {
return true
}
if strings.HasPrefix(key, collectionName+"_") {
return true
}
idx := strings.LastIndex(key, ":")
if idx < 0 {
return false
}
suf := key[idx+1:]
return suf == collectionName || strings.HasPrefix(suf, collectionName+"_")
}
// ClearEntriesContainingIDs удаляет из кэша только те записи, в данных которых
// встречается хотя бы один из переданных id. Если ids пуст — ничего не делает.
func (c *RecordCache) ClearEntriesContainingIDs(ids []any) {
if len(ids) == 0 {
return
}
c.mutex.Lock()
defer c.mutex.Unlock()
for key, entry := range c.cache {
if entry.Data == nil {
continue
}
records, ok := entry.Data.([]any)
if !ok {
continue
}
for _, rec := range records {
m, ok := rec.(map[string]any)
if !ok {
continue
}
recID := m["id"]
for _, id := range ids {
if idsEqual(recID, id) {
delete(c.cache, key)
goto nextKey
}
}
}
nextKey:
}
}
func idsEqual(a, b any) bool {
if a == b {
return true
}
// JSON unmarshals numbers as float64; сравниваем численно
if an, ok := toFloat64(a); ok {
if bn, ok := toFloat64(b); ok {
return an == bn
}
}
return false
}
func toFloat64(v any) (float64, bool) {
switch x := v.(type) {
case float64:
return x, true
case int:
return float64(x), true
case int64:
return float64(x), true
}
return 0, false
}
// Stop останавливает кэш
func (c *RecordCache) Stop() {
close(c.stopChan)
}
// Size возвращает размер кэша
func (c *RecordCache) Size() int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache)
}
// GetFlatCacheMap возвращает плоскую карту кэша
func (c *RecordCache) GetFlatCacheMap() map[string]*CacheEntry {
c.mutex.RLock()
defer c.mutex.RUnlock()
result := make(map[string]*CacheEntry)
for key, entry := range c.cache {
result[key] = entry
}
return result
}
// evictOldest удаляет самую старую запись из кэша
func (c *RecordCache) evictOldest() {
var oldestKey string
var oldestTime time.Time
for key, entry := range c.cache {
if oldestKey == "" || entry.Timestamp.Before(oldestTime) {
oldestKey = key
oldestTime = entry.Timestamp
}
}
if oldestKey != "" {
delete(c.cache, oldestKey)
}
}
// cleanupLoop запускает цикл очистки устаревших записей
func (c *RecordCache) cleanupLoop() {
// Используем более безопасный интервал
interval := c.ttl / 4
if interval < time.Second {
interval = time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.cleanup()
case <-c.stopChan:
return
}
}
}
// cleanup очищает устаревшие записи
func (c *RecordCache) cleanup() {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
for key, entry := range c.cache {
if c.ttl > 0 && now.Sub(entry.Timestamp) > c.ttl {
delete(c.cache, key)
}
}
}
// SetByRecord устанавливает запись в кэш по записи
func (c *RecordCache) SetByRecord(record any, collectionName string) {
key := toString(record) + ":" + collectionName
c.Set(key, record)
}
// UpdateCacheAfterInsert обновляет кэш после вставки
func (c *RecordCache) UpdateCacheAfterInsert(record any, collectionName string) {
c.SetByRecord(record, collectionName)
}
// toString преобразует значение в строку
func toString(value any) string {
switch v := value.(type) {
case string:
return v
case int:
return string(rune(v))
case float64:
return string(rune(int(v)))
case map[string]any:
if id, exists := v["id"]; exists {
return toString(id)
}
return "unknown"
default:
return "unknown"
}
}