291 lines
6.4 KiB
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"
|
|
}
|
|
}
|