github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/lru/disk_lru.go (about) 1 package lru 2 3 import ( 4 "container/list" 5 json "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "sync" 11 "time" 12 13 "github.com/keybase/client/go/libkb" 14 context "golang.org/x/net/context" 15 ) 16 17 type Pathable struct { 18 Path string 19 } 20 21 type DiskLRUEntry struct { 22 Key string 23 Value interface{} 24 Ctime time.Time 25 LastAccessed time.Time 26 } 27 28 type diskLRUIndexMarshaled struct { 29 Version int 30 EntryKeys []string 31 } 32 33 type diskLRUIndex struct { 34 sync.Mutex 35 Version int 36 EntryKeys *list.List 37 entryKeyMap map[string]*list.Element 38 dirty bool 39 } 40 41 func newDiskLRUIndex(version int) *diskLRUIndex { 42 return &diskLRUIndex{ 43 EntryKeys: list.New(), 44 Version: version, 45 entryKeyMap: make(map[string]*list.Element), 46 } 47 } 48 49 func (d *diskLRUIndex) exists(key string) *list.Element { 50 return d.entryKeyMap[key] 51 } 52 53 func (d *diskLRUIndex) Exists(key string) bool { 54 d.Lock() 55 defer d.Unlock() 56 return (d.exists(key) != nil) 57 } 58 59 func (d *diskLRUIndex) remove(key string) { 60 if el, ok := d.entryKeyMap[key]; ok { 61 d.EntryKeys.Remove(el) 62 delete(d.entryKeyMap, key) 63 } 64 } 65 66 func (d *diskLRUIndex) Remove(key string) { 67 d.Lock() 68 defer d.Unlock() 69 d.dirty = true 70 d.remove(key) 71 } 72 73 func (d *diskLRUIndex) put(key string) { 74 d.entryKeyMap[key] = d.EntryKeys.PushFront(key) 75 } 76 77 func (d *diskLRUIndex) Put(key string) { 78 d.Lock() 79 defer d.Unlock() 80 d.dirty = true 81 if d.exists(key) != nil { 82 d.remove(key) 83 } 84 d.put(key) 85 } 86 87 func (d *diskLRUIndex) IsDirty() bool { 88 d.Lock() 89 defer d.Unlock() 90 return d.dirty 91 } 92 93 func (d *diskLRUIndex) ClearDirty() { 94 d.Lock() 95 defer d.Unlock() 96 d.dirty = false 97 } 98 99 func (d *diskLRUIndex) Marshal() diskLRUIndexMarshaled { 100 var m diskLRUIndexMarshaled 101 m.Version = d.Version 102 for e := d.EntryKeys.Front(); e != nil; e = e.Next() { 103 m.EntryKeys = append(m.EntryKeys, e.Value.(string)) 104 } 105 return m 106 } 107 108 func (d *diskLRUIndex) Unmarshal(m diskLRUIndexMarshaled) { 109 d.EntryKeys = list.New() 110 d.Version = m.Version 111 d.entryKeyMap = make(map[string]*list.Element) 112 for _, k := range m.EntryKeys { 113 d.entryKeyMap[k] = d.EntryKeys.PushBack(k) 114 } 115 } 116 117 func (d *diskLRUIndex) Size() int { 118 d.Lock() 119 defer d.Unlock() 120 return d.EntryKeys.Len() 121 } 122 123 func (d *diskLRUIndex) OldestKey() (string, error) { 124 d.Lock() 125 defer d.Unlock() 126 if d.EntryKeys.Len() == 0 { 127 return "", errors.New("index is empty") 128 } 129 return d.EntryKeys.Back().Value.(string), nil 130 } 131 132 // DiskLRU maintains a cache of files on the disk in a LRU manner. 133 type DiskLRU struct { 134 sync.Mutex 135 136 index *diskLRUIndex 137 name string 138 version int 139 maxSize int 140 141 lastFlush time.Time 142 flushDuration time.Duration 143 144 // testing 145 flushCh chan struct{} 146 } 147 148 func NewDiskLRU(name string, version, maxSize int) *DiskLRU { 149 return &DiskLRU{ 150 name: name, 151 version: version, 152 maxSize: maxSize, 153 flushDuration: time.Minute, 154 } 155 } 156 157 func (d *DiskLRU) MaxSize() int { 158 d.Lock() 159 defer d.Unlock() 160 return d.maxSize 161 } 162 163 func (d *DiskLRU) debug(ctx context.Context, lctx libkb.LRUContext, msg string, args ...interface{}) { 164 lctx.GetLog().CDebugf(ctx, fmt.Sprintf("DiskLRU: %s(%d): ", d.name, d.version)+msg, args...) 165 } 166 167 func (d *DiskLRU) indexKey() libkb.DbKey { 168 return libkb.DbKey{ 169 Typ: libkb.DBDiskLRUIndex, 170 Key: fmt.Sprintf("%s:%d", d.name, d.version), 171 } 172 } 173 174 func (d *DiskLRU) entryKey(key string) libkb.DbKey { 175 return libkb.DbKey{ 176 Typ: libkb.DBDiskLRUEntries, 177 Key: fmt.Sprintf("%s:%d:%s", d.name, d.version, key), 178 } 179 } 180 181 func (d *DiskLRU) readIndex(ctx context.Context, lctx libkb.LRUContext) (res *diskLRUIndex, err error) { 182 // Check memory and stash if we read with no error 183 if d.index != nil { 184 return d.index, nil 185 } 186 defer func() { 187 if err == nil && res != nil { 188 d.index = res 189 } 190 }() 191 192 // Grab from the disk if we miss on memory 193 var marshalIndex diskLRUIndexMarshaled 194 res = new(diskLRUIndex) 195 found, err := lctx.GetKVStore().GetInto(&marshalIndex, d.indexKey()) 196 if err != nil { 197 return nil, err 198 } 199 if !found { 200 return newDiskLRUIndex(d.version), nil 201 } 202 res.Unmarshal(marshalIndex) 203 return res, nil 204 } 205 206 func (d *DiskLRU) writeIndex(ctx context.Context, lctx libkb.LRUContext, index *diskLRUIndex, 207 forceFlush bool) error { 208 if forceFlush || lctx.GetClock().Now().Sub(d.lastFlush) > d.flushDuration { 209 marshalIndex := index.Marshal() 210 if err := lctx.GetKVStore().PutObj(d.indexKey(), nil, marshalIndex); err != nil { 211 return err 212 } 213 d.lastFlush = lctx.GetClock().Now() 214 index.ClearDirty() 215 if d.flushCh != nil { 216 d.flushCh <- struct{}{} 217 } 218 } 219 return nil 220 } 221 222 func (d *DiskLRU) readEntry(ctx context.Context, lctx libkb.LRUContext, key string) (found bool, res DiskLRUEntry, err error) { 223 found, err = lctx.GetKVStore().GetInto(&res, d.entryKey(key)) 224 if err != nil { 225 return false, res, err 226 } 227 return found, res, nil 228 } 229 230 func (d *DiskLRU) accessEntry(ctx context.Context, lctx libkb.LRUContext, index *diskLRUIndex, 231 entry *DiskLRUEntry) error { 232 // Promote the key in the index 233 index.Put(entry.Key) 234 // Write out the entry with new accessed time 235 entry.LastAccessed = lctx.GetClock().Now() 236 return lctx.GetKVStore().PutObj(d.entryKey(entry.Key), nil, entry) 237 } 238 239 func (d *DiskLRU) Get(ctx context.Context, lctx libkb.LRUContext, key string) (found bool, res DiskLRUEntry, err error) { 240 d.Lock() 241 defer d.Unlock() 242 243 var index *diskLRUIndex 244 defer func() { 245 // Commit the index 246 if err == nil && index != nil && index.IsDirty() { 247 err := d.writeIndex(ctx, lctx, index, false) 248 if err != nil { 249 d.debug(ctx, lctx, "Get: error writing index: %+v", err) 250 } 251 } 252 }() 253 254 // Grab entry index 255 index, err = d.readIndex(ctx, lctx) 256 if err != nil { 257 return found, res, err 258 } 259 // Check for a straight up miss 260 if !index.Exists(key) { 261 return false, res, nil 262 } 263 // Read entry 264 found, res, err = d.readEntry(ctx, lctx, key) 265 if err != nil { 266 return found, res, err 267 } 268 if !found { 269 // remove from index 270 index.Remove(key) 271 return false, res, nil 272 } 273 // update last accessed time for the entry 274 if err = d.accessEntry(ctx, lctx, index, &res); err != nil { 275 return found, res, err 276 } 277 278 return true, res, nil 279 } 280 281 func (d *DiskLRU) removeEntry(ctx context.Context, lctx libkb.LRUContext, index *diskLRUIndex, key string) error { 282 index.Remove(key) 283 return lctx.GetKVStore().Delete(d.entryKey(key)) 284 } 285 286 func (d *DiskLRU) addEntry(ctx context.Context, lctx libkb.LRUContext, index *diskLRUIndex, key string, 287 value interface{}) (evicted *DiskLRUEntry, err error) { 288 289 // Add the new item 290 index.Put(key) 291 item := DiskLRUEntry{ 292 Key: key, 293 Value: value, 294 Ctime: lctx.GetClock().Now(), 295 LastAccessed: lctx.GetClock().Now(), 296 } 297 if err = lctx.GetKVStore().PutObj(d.entryKey(key), nil, item); err != nil { 298 return nil, err 299 } 300 301 if index.Size() > d.maxSize { 302 // Evict the oldest item 303 var found bool 304 var lastItem DiskLRUEntry 305 lastKey, err := index.OldestKey() 306 if err == nil { 307 d.debug(ctx, lctx, "evicting: %s", lastKey) 308 found, lastItem, err = d.readEntry(ctx, lctx, lastKey) 309 if err != nil { 310 return nil, err 311 } 312 if found { 313 evicted = &lastItem 314 d.debug(ctx, lctx, "addEntry: evicting item: key: %s", lastKey) 315 } 316 if err = d.removeEntry(ctx, lctx, index, lastKey); err != nil { 317 return nil, err 318 } 319 } else { 320 d.debug(ctx, lctx, "addEntry: failed to find oldest key, check cache config") 321 } 322 } 323 324 return evicted, nil 325 } 326 327 func (d *DiskLRU) Put(ctx context.Context, lctx libkb.LRUContext, key string, value interface{}) (evicted *DiskLRUEntry, err error) { 328 d.Lock() 329 defer d.Unlock() 330 331 var index *diskLRUIndex 332 defer func() { 333 // Commit the index 334 if err == nil && index != nil && index.IsDirty() { 335 err = d.writeIndex(ctx, lctx, index, true) 336 } 337 }() 338 339 // Grab entry index 340 index, err = d.readIndex(ctx, lctx) 341 if err != nil { 342 return nil, err 343 } 344 // Remove existing entry from the index (we don't need to remove entry off the disk, since we will 345 // overwrite it with new stuff) 346 if index.Exists(key) { 347 index.Remove(key) 348 } 349 // Add the item 350 return d.addEntry(ctx, lctx, index, key, value) 351 } 352 353 func (d *DiskLRU) Remove(ctx context.Context, lctx libkb.LRUContext, key string) (err error) { 354 d.Lock() 355 defer d.Unlock() 356 var index *diskLRUIndex 357 defer func() { 358 // Commit the index 359 if err == nil && index != nil && index.IsDirty() { 360 err := d.writeIndex(ctx, lctx, index, false) 361 if err != nil { 362 d.debug(ctx, lctx, "Get: error writing index: %+v", err) 363 } 364 365 } 366 }() 367 // Grab entry index 368 index, err = d.readIndex(ctx, lctx) 369 if err != nil { 370 return err 371 } 372 return d.removeEntry(ctx, lctx, index, key) 373 } 374 375 func (d *DiskLRU) ClearMemory(ctx context.Context, lctx libkb.LRUContext) { 376 d.Lock() 377 defer d.Unlock() 378 d.flush(ctx, lctx) 379 d.index = nil 380 } 381 382 func (d *DiskLRU) flush(ctx context.Context, lctx libkb.LRUContext) error { 383 if d.index != nil { 384 return d.writeIndex(ctx, lctx, d.index, true) 385 } 386 return nil 387 } 388 389 func (d *DiskLRU) Flush(ctx context.Context, lctx libkb.LRUContext) error { 390 d.Lock() 391 defer d.Unlock() 392 return d.flush(ctx, lctx) 393 } 394 395 func (d *DiskLRU) Size(ctx context.Context, lctx libkb.LRUContext) (int, error) { 396 d.Lock() 397 defer d.Unlock() 398 index, err := d.readIndex(ctx, lctx) 399 if err != nil { 400 return 0, err 401 } 402 return index.Size(), nil 403 } 404 405 func (d *DiskLRU) allValuesLocked(ctx context.Context, lctx libkb.LRUContext) (entries []DiskLRUEntry, err error) { 406 var index *diskLRUIndex 407 defer func() { 408 // Commit the index 409 if err == nil && index != nil && index.IsDirty() { 410 err := d.writeIndex(ctx, lctx, index, false) 411 if err != nil { 412 d.debug(ctx, lctx, "Get: error writing index: %+v", err) 413 } 414 } 415 }() 416 417 // Grab entry index 418 index, err = d.readIndex(ctx, lctx) 419 if err != nil { 420 return nil, err 421 } 422 for key := range index.entryKeyMap { 423 found, res, err := d.readEntry(ctx, lctx, key) 424 switch { 425 case err != nil: 426 return nil, err 427 case !found: 428 index.Remove(key) 429 default: 430 entries = append(entries, res) 431 } 432 } 433 return entries, nil 434 } 435 436 func (d *DiskLRU) CleanOutOfSync(mctx libkb.MetaContext, cacheDir string) error { 437 _, err := d.cleanOutOfSync(mctx, cacheDir, 0) 438 return err 439 } 440 441 func (d *DiskLRU) getPath(entry DiskLRUEntry) (res string, ok bool) { 442 if res, ok = entry.Value.(string); ok { 443 return res, ok 444 } 445 if _, ok = entry.Value.(map[string]interface{}); ok { 446 var pathable Pathable 447 jstr, _ := json.Marshal(entry.Value) 448 _ = json.Unmarshal(jstr, &pathable) 449 path := pathable.Path 450 if len(path) == 0 { 451 return "", false 452 } 453 return path, true 454 } 455 return "", false 456 } 457 458 func (d *DiskLRU) cleanOutOfSync(mctx libkb.MetaContext, cacheDir string, batchSize int) (completed bool, err error) { 459 defer mctx.Trace("cleanOutOfSync", &err)() 460 d.Lock() 461 defer d.Unlock() 462 463 // clear our inmemory cache without flushing to disk to force a new read 464 d.index = nil 465 466 // reverse map of filepaths to lru keys 467 cacheRevMap := map[string]string{} 468 allVals, err := d.allValuesLocked(mctx.Ctx(), mctx.G()) 469 if err != nil { 470 return false, err 471 } 472 for _, entry := range allVals { 473 path, ok := d.getPath(entry) 474 if !ok { 475 continue 476 } 477 // normalize the filepath in case the abs path to of the cacheDir 478 // changed. 479 path = filepath.Join(cacheDir, filepath.Base(path)) 480 cacheRevMap[path] = entry.Key 481 } 482 483 files, err := filepath.Glob(filepath.Join(cacheDir, "*")) 484 if err != nil { 485 return false, err 486 } 487 488 d.debug(mctx.Ctx(), mctx.G(), "Clean: found %d files in %s, %d in cache", 489 len(files), cacheDir, len(cacheRevMap)) 490 removed := 0 491 for _, v := range files { 492 if _, ok := cacheRevMap[v]; !ok { 493 if err := os.Remove(v); err != nil { 494 d.debug(mctx.Ctx(), mctx.G(), "Clean: failed to delete file %q: %s", v, err) 495 } 496 removed++ 497 if batchSize > 0 && removed > batchSize { 498 d.debug(mctx.Ctx(), mctx.G(), "Clean: Aborting clean, reached batch size %d", batchSize) 499 return false, nil 500 } 501 } 502 } 503 return true, nil 504 } 505 506 // CleanOutOfSyncWithDelay runs the LRU clean function after the `delay` duration. If 507 // the service crashes it's possible that temporarily files get stranded on 508 // disk before they can get recorded in the LRU. Callers can run this in the 509 // background to prevent leaking space. We delay to keep off the critical path 510 // to start up. 511 func CleanOutOfSyncWithDelay(mctx libkb.MetaContext, d *DiskLRU, cacheDir string, delay time.Duration) { 512 513 mctx.Debug("CleanOutOfSyncWithDelay: cleaning %s in %v", cacheDir, delay) 514 select { 515 case <-mctx.Ctx().Done(): 516 mctx.Debug("CleanOutOfSyncWithDelay: cancelled before initial delay finished") 517 return 518 case <-time.After(delay): 519 } 520 521 defer mctx.Trace("CleanOutOfSyncWithDelay", nil)() 522 523 // Batch deletions so we don't hog the lock. 524 batchSize := 1000 525 526 batchDelay := 10 * time.Millisecond 527 if mctx.G().IsMobileAppType() { 528 batchDelay = 25 * time.Millisecond 529 } 530 for { 531 select { 532 case <-mctx.Ctx().Done(): 533 mctx.Debug("CleanOutOfSyncWithDelay: cancelled") 534 return 535 default: 536 } 537 if completed, err := d.cleanOutOfSync(mctx, cacheDir, batchSize); err != nil { 538 mctx.Debug("unable to run clean: %v", err) 539 break 540 } else if completed { 541 break 542 } 543 // Keep out of a tight loop with a short sleep. 544 time.Sleep(batchDelay) 545 } 546 size, err := d.Size(mctx.Ctx(), mctx.G()) 547 if err != nil { 548 mctx.Debug("unable to get diskLRU size: %v", err) 549 } 550 mctx.Debug("lru current size: %d, max size: %d", size, d.MaxSize()) 551 }