You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
4.6 KiB
163 lines
4.6 KiB
//go:build !windows |
|
|
|
package storage |
|
|
|
import ( |
|
"container/list" |
|
"context" |
|
"sync" |
|
|
|
"lol.mleku.dev/log" |
|
) |
|
|
|
// AccessTrackerDatabase defines the interface for the underlying database |
|
// that stores access tracking information. |
|
type AccessTrackerDatabase interface { |
|
RecordEventAccess(serial uint64, connectionID string) error |
|
GetEventAccessInfo(serial uint64) (lastAccess int64, accessCount uint32, err error) |
|
GetLeastAccessedEvents(limit int, minAgeSec int64) (serials []uint64, err error) |
|
} |
|
|
|
// accessKey is the composite key for deduplication: serial + connectionID |
|
type accessKey struct { |
|
Serial uint64 |
|
ConnectionID string |
|
} |
|
|
|
// AccessTracker tracks event access patterns with session deduplication. |
|
// It maintains an in-memory cache to deduplicate accesses from the same |
|
// connection, reducing database writes while ensuring unique session counting. |
|
type AccessTracker struct { |
|
db AccessTrackerDatabase |
|
|
|
// Deduplication cache: tracks which (serial, connectionID) pairs |
|
// have already been recorded in this session window |
|
mu sync.RWMutex |
|
seen map[accessKey]struct{} |
|
seenOrder *list.List // LRU order for eviction |
|
seenElements map[accessKey]*list.Element |
|
maxSeen int // Maximum entries in dedup cache |
|
|
|
// Flush interval for stats |
|
ctx context.Context |
|
cancel context.CancelFunc |
|
} |
|
|
|
// NewAccessTracker creates a new access tracker. |
|
// maxSeenEntries controls the size of the deduplication cache. |
|
func NewAccessTracker(db AccessTrackerDatabase, maxSeenEntries int) *AccessTracker { |
|
if maxSeenEntries <= 0 { |
|
maxSeenEntries = 100000 // Default: 100k entries |
|
} |
|
|
|
ctx, cancel := context.WithCancel(context.Background()) |
|
|
|
return &AccessTracker{ |
|
db: db, |
|
seen: make(map[accessKey]struct{}), |
|
seenOrder: list.New(), |
|
seenElements: make(map[accessKey]*list.Element), |
|
maxSeen: maxSeenEntries, |
|
ctx: ctx, |
|
cancel: cancel, |
|
} |
|
} |
|
|
|
// RecordAccess records an access to an event by a connection. |
|
// Deduplicates accesses from the same connection within the cache window. |
|
// Returns true if this was a new access, false if deduplicated. |
|
func (t *AccessTracker) RecordAccess(serial uint64, connectionID string) (bool, error) { |
|
key := accessKey{Serial: serial, ConnectionID: connectionID} |
|
|
|
t.mu.Lock() |
|
// Check if already seen |
|
if _, exists := t.seen[key]; exists { |
|
// Move to front (most recent) |
|
if elem, ok := t.seenElements[key]; ok { |
|
t.seenOrder.MoveToFront(elem) |
|
} |
|
t.mu.Unlock() |
|
return false, nil // Deduplicated |
|
} |
|
|
|
// Evict oldest if at capacity |
|
if len(t.seen) >= t.maxSeen { |
|
oldest := t.seenOrder.Back() |
|
if oldest != nil { |
|
oldKey := oldest.Value.(accessKey) |
|
delete(t.seen, oldKey) |
|
delete(t.seenElements, oldKey) |
|
t.seenOrder.Remove(oldest) |
|
} |
|
} |
|
|
|
// Add to cache |
|
t.seen[key] = struct{}{} |
|
elem := t.seenOrder.PushFront(key) |
|
t.seenElements[key] = elem |
|
t.mu.Unlock() |
|
|
|
// Record to database |
|
if err := t.db.RecordEventAccess(serial, connectionID); err != nil { |
|
return true, err |
|
} |
|
|
|
return true, nil |
|
} |
|
|
|
// GetAccessInfo returns the access information for an event. |
|
func (t *AccessTracker) GetAccessInfo(serial uint64) (lastAccess int64, accessCount uint32, err error) { |
|
return t.db.GetEventAccessInfo(serial) |
|
} |
|
|
|
// GetColdestEvents returns event serials sorted by coldness. |
|
// limit: max events to return |
|
// minAgeSec: minimum age in seconds since last access |
|
func (t *AccessTracker) GetColdestEvents(limit int, minAgeSec int64) ([]uint64, error) { |
|
return t.db.GetLeastAccessedEvents(limit, minAgeSec) |
|
} |
|
|
|
// ClearConnection removes all dedup entries for a specific connection. |
|
// Call this when a connection closes to free up cache space. |
|
func (t *AccessTracker) ClearConnection(connectionID string) { |
|
t.mu.Lock() |
|
defer t.mu.Unlock() |
|
|
|
// Find and remove all entries for this connection |
|
for key, elem := range t.seenElements { |
|
if key.ConnectionID == connectionID { |
|
delete(t.seen, key) |
|
delete(t.seenElements, key) |
|
t.seenOrder.Remove(elem) |
|
} |
|
} |
|
} |
|
|
|
// Stats returns current cache statistics. |
|
func (t *AccessTracker) Stats() AccessTrackerStats { |
|
t.mu.RLock() |
|
defer t.mu.RUnlock() |
|
|
|
return AccessTrackerStats{ |
|
CachedEntries: len(t.seen), |
|
MaxEntries: t.maxSeen, |
|
} |
|
} |
|
|
|
// AccessTrackerStats holds access tracker statistics. |
|
type AccessTrackerStats struct { |
|
CachedEntries int |
|
MaxEntries int |
|
} |
|
|
|
// Start starts any background goroutines for the tracker. |
|
// Currently a no-op but provided for future use. |
|
func (t *AccessTracker) Start() { |
|
log.I.F("access tracker started with %d max dedup entries", t.maxSeen) |
|
} |
|
|
|
// Stop stops the access tracker and releases resources. |
|
func (t *AccessTracker) Stop() { |
|
t.cancel() |
|
log.I.F("access tracker stopped") |
|
}
|
|
|