go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/caching/cache/lru.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cache 16 17 import ( 18 "container/list" 19 "crypto" 20 "encoding/json" 21 "fmt" 22 "time" 23 24 "go.chromium.org/luci/common/data/text/units" 25 ) 26 27 // entry is an entry in the orderedDict. 28 type entry struct { 29 key HexDigest 30 value units.Size 31 lastAccess int64 // UTC time 32 } 33 34 func (e *entry) MarshalJSON() ([]byte, error) { 35 // encode as a tuple. 36 return json.Marshal([]any{e.key, []any{e.value, e.lastAccess}}) 37 } 38 39 func (e *entry) UnmarshalJSON(data []byte) error { 40 // decode from tuple. 41 var elems []any 42 if err := json.Unmarshal(data, &elems); err != nil { 43 return fmt.Errorf("invalid entry: %s: %s", err, string(data)) 44 } 45 if len(elems) != 2 { 46 return fmt.Errorf("invalid entry: expected 2 items: %s", string(data)) 47 } 48 if key, ok := elems[0].(string); ok { 49 e.key = HexDigest(key) 50 values, ok := elems[1].([]any) 51 if !ok { 52 return fmt.Errorf("invalid entry: expected array for second element: %s", string(data)) 53 } 54 55 if len(values) != 2 { 56 return fmt.Errorf("invalid entry: expected 2 items: %v", values) 57 } 58 59 if value, ok := values[0].(float64); ok { 60 e.value = units.Size(value) 61 } else { 62 return fmt.Errorf("invalid entry: expected value to be number: %#v", values[0]) 63 } 64 65 if value, ok := values[1].(float64); ok { 66 e.lastAccess = int64(value) 67 } else { 68 return fmt.Errorf("invalid entry: expected value to be number: %#v", values[1]) 69 } 70 } else { 71 return fmt.Errorf("invalid entry: expected key to be string: %s", string(data)) 72 } 73 return nil 74 } 75 76 // orderedDict implements a dict that keeps ordering. 77 type orderedDict struct { 78 ll *list.List 79 entries map[HexDigest]*list.Element 80 } 81 82 func makeOrderedDict() orderedDict { 83 return orderedDict{ 84 ll: list.New(), 85 entries: map[HexDigest]*list.Element{}, 86 } 87 } 88 89 // keys returns the keys in order. 90 func (o *orderedDict) keys() HexDigests { 91 out := make(HexDigests, 0, o.length()) 92 for e := o.ll.Front(); e != nil; e = e.Next() { 93 out = append(out, e.Value.(*entry).key) 94 } 95 return out 96 } 97 98 func (o *orderedDict) pop(key HexDigest) (units.Size, bool) { 99 if e, hit := o.entries[key]; hit { 100 return o.removeElement(e).value, true 101 } 102 return 0, false 103 } 104 105 func (o *orderedDict) popOldest() (HexDigest, units.Size) { 106 if e := o.ll.Back(); e != nil { 107 entry := o.removeElement(e) 108 return entry.key, entry.value 109 } 110 return "", 0 111 } 112 113 func (o *orderedDict) removeElement(e *list.Element) *entry { 114 o.ll.Remove(e) 115 kv := e.Value.(*entry) 116 delete(o.entries, kv.key) 117 return kv 118 } 119 120 func (o *orderedDict) length() int { 121 return o.ll.Len() 122 } 123 124 func (o *orderedDict) pushFront(key HexDigest, value units.Size) { 125 if e, ok := o.entries[key]; ok { 126 o.ll.MoveToFront(e) 127 e.Value.(*entry).value = value 128 e.Value.(*entry).lastAccess = time.Now().Unix() 129 return 130 } 131 o.entries[key] = o.ll.PushFront(&entry{key, value, time.Now().Unix()}) 132 } 133 134 func (o *orderedDict) pushBack(key HexDigest, value units.Size, lastAccess int64) { 135 if e, ok := o.entries[key]; ok { 136 o.ll.MoveToBack(e) 137 e.Value.(*entry).value = value 138 e.Value.(*entry).lastAccess = lastAccess 139 return 140 } 141 o.entries[key] = o.ll.PushBack(&entry{key, value, lastAccess}) 142 } 143 144 // serialized returns all the items in order. 145 func (o *orderedDict) serialized() []entry { 146 out := make([]entry, 0, o.length()) 147 for e := o.ll.Front(); e != nil; e = e.Next() { 148 out = append(out, *e.Value.(*entry)) 149 } 150 return out 151 } 152 153 // lruDict is a dictionary that evicts least recently used items. It is a 154 // higher level than orderedDict. 155 // 156 // Designed to be serialized as JSON on disk. 157 type lruDict struct { 158 h crypto.Hash 159 items orderedDict // ordered key -> value mapping, newest items at the bottom. 160 dirty bool // true if was modified after loading until it is marshaled. 161 sum units.Size // sum of all the values. 162 } 163 164 func makeLRUDict(h crypto.Hash) lruDict { 165 return lruDict{ 166 h: h, 167 items: makeOrderedDict(), 168 } 169 } 170 171 func (l *lruDict) IsDirty() bool { 172 return l.dirty 173 } 174 175 func (l *lruDict) keys() HexDigests { 176 return l.items.keys() 177 } 178 179 func (l *lruDict) length() int { 180 return l.items.length() 181 } 182 183 func (l *lruDict) pop(key HexDigest) (units.Size, bool) { 184 out, b := l.items.pop(key) 185 l.sum -= out 186 l.dirty = true 187 return out, b 188 } 189 190 func (l *lruDict) popOldest() (HexDigest, units.Size) { 191 k, v := l.items.popOldest() 192 l.sum -= v 193 if k != "" { 194 l.dirty = true 195 } 196 return k, v 197 } 198 199 func (l *lruDict) pushFront(key HexDigest, value units.Size) { 200 l.items.pushFront(key, value) 201 l.sum += value 202 l.dirty = true 203 } 204 205 func (l *lruDict) touch(key HexDigest) bool { 206 l.dirty = true 207 if value, b := l.items.pop(key); b { 208 l.items.pushFront(key, value) 209 return true 210 } 211 return false 212 } 213 214 type serializedLRUDict struct { 215 Version int `json:"version"` // 1. 216 Items []entry `json:"items"` // ordered key -> value mapping in order. 217 } 218 219 const currentVersion = 3 220 221 func (l *lruDict) MarshalJSON() ([]byte, error) { 222 s := &serializedLRUDict{ 223 Version: currentVersion, 224 Items: l.items.serialized(), 225 } 226 // Not strictly true but #closeneough. 227 content, err := json.Marshal(s) 228 if err == nil { 229 l.dirty = false 230 } 231 return content, err 232 } 233 234 func (l *lruDict) UnmarshalJSON(data []byte) error { 235 s := &serializedLRUDict{} 236 if err := json.Unmarshal(data, &s); err != nil { 237 return err 238 } 239 240 if s.Version != currentVersion { 241 return fmt.Errorf("invalid lru dict version %d instead of current version %d", s.Version, currentVersion) 242 } 243 l.sum = 0 244 for _, e := range s.Items { 245 if !e.key.Validate(l.h) { 246 return fmt.Errorf("invalid entry: %s", e.key) 247 } 248 l.items.pushBack(e.key, e.value, e.lastAccess) 249 l.sum += e.value 250 } 251 l.dirty = false 252 return nil 253 }