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