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  }