access_tracking.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"encoding/binary"
   7  	"sort"
   8  	"time"
   9  
  10  	"github.com/dgraph-io/badger/v4"
  11  )
  12  
  13  const (
  14  	// accessTrackingPrefix is the key prefix for access tracking records.
  15  	// Key format: acc:{8-byte serial} -> {8-byte lastAccessTime}{4-byte accessCount}
  16  	accessTrackingPrefix = "acc:"
  17  )
  18  
  19  // RecordEventAccess updates access tracking for an event.
  20  // This increments the access count and updates the last access time.
  21  // The connectionID is currently not used for deduplication in the database layer,
  22  // but is passed for potential future use. Deduplication is handled in the
  23  // higher-level AccessTracker which maintains an in-memory cache.
  24  func (d *D) RecordEventAccess(serial uint64, connectionID string) error {
  25  	key := d.accessKey(serial)
  26  
  27  	return d.Update(func(txn *badger.Txn) error {
  28  		var lastAccess int64
  29  		var accessCount uint32
  30  
  31  		// Try to get existing record
  32  		item, err := txn.Get(key)
  33  		if err == nil {
  34  			err = item.Value(func(val []byte) error {
  35  				if len(val) >= 12 {
  36  					lastAccess = int64(binary.BigEndian.Uint64(val[0:8]))
  37  					accessCount = binary.BigEndian.Uint32(val[8:12])
  38  				}
  39  				return nil
  40  			})
  41  			if err != nil {
  42  				return err
  43  			}
  44  		} else if err != badger.ErrKeyNotFound {
  45  			return err
  46  		}
  47  
  48  		// Update values
  49  		_ = lastAccess // unused in simple increment mode
  50  		lastAccess = time.Now().Unix()
  51  		accessCount++
  52  
  53  		// Write back
  54  		val := make([]byte, 12)
  55  		binary.BigEndian.PutUint64(val[0:8], uint64(lastAccess))
  56  		binary.BigEndian.PutUint32(val[8:12], accessCount)
  57  
  58  		return txn.Set(key, val)
  59  	})
  60  }
  61  
  62  // GetEventAccessInfo returns access information for an event.
  63  // Returns (0, 0, nil) if the event has never been accessed.
  64  func (d *D) GetEventAccessInfo(serial uint64) (lastAccess int64, accessCount uint32, err error) {
  65  	key := d.accessKey(serial)
  66  
  67  	err = d.View(func(txn *badger.Txn) error {
  68  		item, gerr := txn.Get(key)
  69  		if gerr != nil {
  70  			if gerr == badger.ErrKeyNotFound {
  71  				// Not found is not an error - just return zeros
  72  				return nil
  73  			}
  74  			return gerr
  75  		}
  76  
  77  		return item.Value(func(val []byte) error {
  78  			if len(val) >= 12 {
  79  				lastAccess = int64(binary.BigEndian.Uint64(val[0:8]))
  80  				accessCount = binary.BigEndian.Uint32(val[8:12])
  81  			}
  82  			return nil
  83  		})
  84  	})
  85  
  86  	return
  87  }
  88  
  89  // accessEntry holds access metadata for sorting
  90  type accessEntry struct {
  91  	serial     uint64
  92  	lastAccess int64
  93  	count      uint32
  94  }
  95  
  96  // GetLeastAccessedEvents returns event serials sorted by coldness.
  97  // Events with older last access times and lower access counts are returned first.
  98  // limit: maximum number of events to return
  99  // minAgeSec: minimum age in seconds since last access (events accessed more recently are excluded)
 100  func (d *D) GetLeastAccessedEvents(limit int, minAgeSec int64) (serials []uint64, err error) {
 101  	cutoffTime := time.Now().Unix() - minAgeSec
 102  
 103  	var entries []accessEntry
 104  
 105  	err = d.View(func(txn *badger.Txn) error {
 106  		prefix := []byte(accessTrackingPrefix)
 107  		opts := badger.DefaultIteratorOptions
 108  		opts.Prefix = prefix
 109  		opts.PrefetchValues = true
 110  		it := txn.NewIterator(opts)
 111  		defer it.Close()
 112  
 113  		for it.Rewind(); it.Valid(); it.Next() {
 114  			item := it.Item()
 115  			key := item.Key()
 116  
 117  			// Extract serial from key (after prefix)
 118  			if len(key) <= len(prefix) {
 119  				continue
 120  			}
 121  			serial := binary.BigEndian.Uint64(key[len(prefix):])
 122  
 123  			var lastAccess int64
 124  			var accessCount uint32
 125  
 126  			err := item.Value(func(val []byte) error {
 127  				if len(val) >= 12 {
 128  					lastAccess = int64(binary.BigEndian.Uint64(val[0:8]))
 129  					accessCount = binary.BigEndian.Uint32(val[8:12])
 130  				}
 131  				return nil
 132  			})
 133  			if err != nil {
 134  				continue
 135  			}
 136  
 137  			// Only include events older than cutoff
 138  			if lastAccess < cutoffTime {
 139  				entries = append(entries, accessEntry{serial, lastAccess, accessCount})
 140  			}
 141  		}
 142  		return nil
 143  	})
 144  
 145  	if err != nil {
 146  		return nil, err
 147  	}
 148  
 149  	// Sort by coldness score (older + fewer accesses = colder = lower score)
 150  	// Score = lastAccess + (accessCount * 3600)
 151  	// Lower score = colder = evict first
 152  	sort.Slice(entries, func(i, j int) bool {
 153  		scoreI := entries[i].lastAccess + int64(entries[i].count)*3600
 154  		scoreJ := entries[j].lastAccess + int64(entries[j].count)*3600
 155  		return scoreI < scoreJ
 156  	})
 157  
 158  	// Return up to limit
 159  	for i := 0; i < len(entries) && i < limit; i++ {
 160  		serials = append(serials, entries[i].serial)
 161  	}
 162  
 163  	return serials, nil
 164  }
 165  
 166  // accessKey generates the database key for an access tracking record.
 167  func (d *D) accessKey(serial uint64) []byte {
 168  	key := make([]byte, len(accessTrackingPrefix)+8)
 169  	copy(key, accessTrackingPrefix)
 170  	binary.BigEndian.PutUint64(key[len(accessTrackingPrefix):], serial)
 171  	return key
 172  }
 173  
 174  // DeleteAccessRecord removes the access tracking record for an event.
 175  // This should be called when an event is deleted.
 176  func (d *D) DeleteAccessRecord(serial uint64) error {
 177  	key := d.accessKey(serial)
 178  
 179  	return d.Update(func(txn *badger.Txn) error {
 180  		return txn.Delete(key)
 181  	})
 182  }
 183