github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/leveldb_cleaner.go (about)

     1  package libkb
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	humanize "github.com/dustin/go-humanize"
    11  	lru "github.com/hashicorp/golang-lru"
    12  	keybase1 "github.com/keybase/client/go/protocol/keybase1"
    13  	"github.com/syndtr/goleveldb/leveldb"
    14  	"github.com/syndtr/goleveldb/leveldb/opt"
    15  	"github.com/syndtr/goleveldb/leveldb/util"
    16  	"golang.org/x/net/context"
    17  )
    18  
    19  type DbCleanerConfig struct {
    20  	// start cleaning if above this size
    21  	MaxSize uint64
    22  	// stop cleaning when below this size
    23  	HaltSize uint64
    24  	// attempt a clean with this frequency
    25  	CleanInterval time.Duration
    26  	// number of keys to keep cached
    27  	CacheCapacity int
    28  	// number of keys cached to run a clean
    29  	MinCacheSize int
    30  	// duration cleaner sleeps when cleaning
    31  	SleepInterval time.Duration
    32  }
    33  
    34  func (c DbCleanerConfig) String() string {
    35  	return fmt.Sprintf("DbCleanerConfig{MaxSize: %v, HaltSize: %v, CleanInterval: %v, CacheCapacity: %v, MinCacheSize: %v, SleepInterval: %v}",
    36  		humanize.Bytes(c.MaxSize), humanize.Bytes(c.HaltSize),
    37  		c.CleanInterval, c.CacheCapacity,
    38  		c.MinCacheSize, c.SleepInterval)
    39  }
    40  
    41  var DefaultMobileDbCleanerConfig = DbCleanerConfig{
    42  	MaxSize:       opt.GiB,
    43  	HaltSize:      opt.GiB * .75,
    44  	CleanInterval: time.Hour,
    45  	CacheCapacity: 100000,
    46  	MinCacheSize:  10000,
    47  	SleepInterval: 10 * time.Millisecond,
    48  }
    49  
    50  var DefaultDesktopDbCleanerConfig = DbCleanerConfig{
    51  	MaxSize:       2 * opt.GiB,
    52  	HaltSize:      1.5 * opt.GiB,
    53  	CleanInterval: time.Hour,
    54  	CacheCapacity: 100000,
    55  	MinCacheSize:  10000,
    56  	SleepInterval: 50 * time.Millisecond,
    57  }
    58  
    59  type levelDbCleaner struct {
    60  	MetaContextified
    61  	sync.Mutex
    62  
    63  	running  bool
    64  	lastKey  []byte
    65  	lastRun  time.Time
    66  	dbName   string
    67  	config   DbCleanerConfig
    68  	cache    *lru.Cache
    69  	cacheMu  sync.Mutex // protects the pointer to the cache
    70  	isMobile bool
    71  	db       *leveldb.DB
    72  	stopCh   chan struct{}
    73  	cancelCh chan struct{}
    74  
    75  	isShutdown bool
    76  }
    77  
    78  func newLevelDbCleaner(mctx MetaContext, dbName string) *levelDbCleaner {
    79  	config := DefaultDesktopDbCleanerConfig
    80  	isMobile := mctx.G().IsMobileAppType()
    81  	if isMobile {
    82  		config = DefaultMobileDbCleanerConfig
    83  	}
    84  	return newLevelDbCleanerWithConfig(mctx, dbName, config, isMobile)
    85  }
    86  
    87  func newLevelDbCleanerWithConfig(mctx MetaContext, dbName string, config DbCleanerConfig, isMobile bool) *levelDbCleaner {
    88  	cache, err := lru.New(config.CacheCapacity)
    89  	if err != nil {
    90  		panic(err)
    91  	}
    92  	mctx = mctx.WithLogTag("DBCLN")
    93  	c := &levelDbCleaner{
    94  		MetaContextified: NewMetaContextified(mctx),
    95  		// Start the run shortly after starting but not immediately
    96  		lastRun:  mctx.G().GetClock().Now().Add(-(config.CleanInterval - config.CleanInterval/10)),
    97  		dbName:   dbName,
    98  		config:   config,
    99  		cache:    cache,
   100  		isMobile: isMobile,
   101  		stopCh:   make(chan struct{}),
   102  		cancelCh: make(chan struct{}),
   103  	}
   104  	if isMobile {
   105  		go c.monitorAppState()
   106  	}
   107  	return c
   108  }
   109  
   110  func (c *levelDbCleaner) getCache() *lru.Cache {
   111  	c.cacheMu.Lock()
   112  	defer c.cacheMu.Unlock()
   113  	return c.cache
   114  }
   115  
   116  func (c *levelDbCleaner) Status() string {
   117  	return fmt.Sprintf("levelDbCleaner{cacheSize: %d, lastRun: %v, lastKey: %v, running: %v}\n%v\n",
   118  		c.cache.Len(), c.lastRun, c.lastKey, c.running, c.config)
   119  }
   120  
   121  func (c *levelDbCleaner) Stop() {
   122  	c.log("Stop")
   123  	c.Lock()
   124  	defer c.Unlock()
   125  	if c.stopCh != nil {
   126  		close(c.stopCh)
   127  		c.stopCh = make(chan struct{})
   128  	}
   129  }
   130  
   131  func (c *levelDbCleaner) monitorAppState() {
   132  	c.log("monitorAppState")
   133  	state := keybase1.MobileAppState_FOREGROUND
   134  	for {
   135  		select {
   136  		case state = <-c.G().MobileAppState.NextUpdate(&state):
   137  			switch state {
   138  			case keybase1.MobileAppState_BACKGROUNDACTIVE:
   139  			default:
   140  				c.log("monitorAppState: attempting cancel, state: %v", state)
   141  				c.Lock()
   142  				if c.cancelCh != nil {
   143  					close(c.cancelCh)
   144  					c.cancelCh = make(chan struct{})
   145  				}
   146  				c.Unlock()
   147  			}
   148  		case <-c.stopCh:
   149  			c.log("monitorAppState: stop")
   150  			return
   151  		}
   152  	}
   153  }
   154  
   155  func (c *levelDbCleaner) log(format string, args ...interface{}) {
   156  	c.M().Debug(fmt.Sprintf("levelDbCleaner(%s): %s", c.dbName, format), args...)
   157  }
   158  
   159  func (c *levelDbCleaner) setDb(db *leveldb.DB) {
   160  	c.Lock()
   161  	defer c.Unlock()
   162  	c.db = db
   163  }
   164  
   165  func (c *levelDbCleaner) cacheKey(key []byte) string {
   166  	return string(key)
   167  }
   168  
   169  func (c *levelDbCleaner) clearCache() {
   170  	c.cache.Purge()
   171  }
   172  
   173  func (c *levelDbCleaner) Shutdown() {
   174  	c.cacheMu.Lock()
   175  	defer c.cacheMu.Unlock()
   176  	c.cache, _ = lru.New(1)
   177  	c.isShutdown = true
   178  }
   179  
   180  func (c *levelDbCleaner) shouldCleanLocked(force bool) bool {
   181  	if c.running {
   182  		return false
   183  	}
   184  	if force {
   185  		return true
   186  	}
   187  	validCache := c.getCache().Len() >= c.config.MinCacheSize
   188  	return validCache &&
   189  		c.G().GetClock().Now().Sub(c.lastRun) >= c.config.CleanInterval
   190  }
   191  
   192  func (c *levelDbCleaner) getDbSize() (size uint64, err error) {
   193  	if c.db == nil {
   194  		return 0, nil
   195  	}
   196  	// get the size from the start of the kv table to the beginning of the perm
   197  	// table since that is all we can clean
   198  	dbRange := util.Range{Start: tablePrefix(levelDbTableKv), Limit: tablePrefix(levelDbTablePerm)}
   199  	sizes, err := c.db.SizeOf([]util.Range{dbRange})
   200  	if err != nil {
   201  		return 0, err
   202  	}
   203  	return uint64(sizes.Sum()), nil
   204  }
   205  
   206  func (c *levelDbCleaner) clean(force bool) (err error) {
   207  	c.Lock()
   208  	// get out without spamming the logs
   209  	if !c.shouldCleanLocked(force) {
   210  		c.Unlock()
   211  		return nil
   212  	}
   213  	c.running = true
   214  	key := c.lastKey
   215  	c.Unlock()
   216  
   217  	defer c.M().Trace(fmt.Sprintf("levelDbCleaner(%s) clean, config: %v", c.dbName, c.config), &err)()
   218  	defer func() {
   219  		c.Lock()
   220  		defer c.Unlock()
   221  		c.lastKey = key
   222  		c.lastRun = c.G().GetClock().Now()
   223  		c.running = false
   224  	}()
   225  
   226  	dbSize, err := c.getDbSize()
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	c.log("dbSize: %v, cacheSize: %v",
   232  		humanize.Bytes(dbSize), c.getCache().Len())
   233  	// check db size, abort if small enough
   234  	if !force && dbSize < c.config.MaxSize {
   235  		return nil
   236  	}
   237  
   238  	var totalNumPurged, numPurged int
   239  	for i := 0; i < 100; i++ {
   240  		select {
   241  		case <-c.cancelCh:
   242  			c.log("aborting clean, %d runs, canceled", i)
   243  			return nil
   244  		case <-c.stopCh:
   245  			c.log("aborting clean %d runs, stopped", i)
   246  			return nil
   247  		default:
   248  		}
   249  
   250  		start := c.G().GetClock().Now()
   251  		numPurged, key, err = c.cleanBatch(key)
   252  		if err != nil {
   253  			return err
   254  		}
   255  		if numPurged == 0 {
   256  			break
   257  		}
   258  		totalNumPurged += numPurged
   259  
   260  		if i%10 == 0 {
   261  			c.log("purged %d items, dbSize: %v, lastKey:%s, ran in: %v",
   262  				numPurged, humanize.Bytes(dbSize), key, c.G().GetClock().Now().Sub(start))
   263  		}
   264  		// check if we are within limits
   265  		dbSize, err = c.getDbSize()
   266  		if err != nil {
   267  			return err
   268  		}
   269  		// check db size, abort if small enough
   270  		if !force && dbSize < c.config.HaltSize {
   271  			break
   272  		}
   273  		time.Sleep(c.config.SleepInterval)
   274  	}
   275  	c.log("clean complete. purged %d items total, dbSize: %v", totalNumPurged, humanize.Bytes(dbSize))
   276  	return nil
   277  }
   278  
   279  func (c *levelDbCleaner) cleanBatch(startKey []byte) (int, []byte, error) {
   280  	// Start our range from wherever we left off last time, and clean up until
   281  	// the permanent entries table begins.
   282  	iterRange := &util.Range{Start: startKey, Limit: tablePrefix(levelDbTablePerm)}
   283  	// Option suggested in
   284  	// https://github.com/google/leveldb/blob/master/doc/index.md#cache
   285  	// """When performing a bulk read, the application may wish to disable
   286  	// caching so that the data processed by the bulk read does not end up
   287  	// displacing most of the cached contents."""
   288  	opts := &opt.ReadOptions{DontFillCache: true}
   289  	iter := c.db.NewIterator(iterRange, opts)
   290  	batch := new(leveldb.Batch)
   291  	for batch.Len() < 1000 && iter.Next() {
   292  		key := iter.Key()
   293  
   294  		c.cacheMu.Lock()
   295  		if c.isShutdown {
   296  			c.cacheMu.Unlock()
   297  			return 0, nil, errors.New("cleanBatch: cancelled due to shutdown")
   298  		}
   299  		cache := c.cache
   300  		c.cacheMu.Unlock()
   301  
   302  		if _, found := cache.Get(c.cacheKey(key)); !found {
   303  			cp := make([]byte, len(key))
   304  			copy(cp, key)
   305  			batch.Delete(cp)
   306  		} else {
   307  			// clear out the value from the lru
   308  			cache.Remove(c.cacheKey(key))
   309  		}
   310  	}
   311  	key := make([]byte, len(iter.Key()))
   312  	copy(key, iter.Key())
   313  	// see if we have reached the end of the db, if so explicitly reset the
   314  	// key value
   315  	iter.Last()
   316  	if bytes.Equal(key, iter.Key()) {
   317  		key = nil
   318  	}
   319  	iter.Release()
   320  	if err := iter.Error(); err != nil {
   321  		return 0, nil, err
   322  	}
   323  	if err := c.db.Write(batch, nil); err != nil {
   324  		return 0, nil, err
   325  	}
   326  	// Compact the range we just deleted in so the size changes are reflected
   327  	err := c.db.CompactRange(util.Range{Start: startKey, Limit: key})
   328  	return batch.Len(), key, err
   329  }
   330  
   331  func (c *levelDbCleaner) attemptClean(ctx context.Context) {
   332  	go func() {
   333  		if err := c.clean(false /*force */); err != nil {
   334  			c.log("unable to clean: %v", err)
   335  		}
   336  	}()
   337  }
   338  
   339  func (c *levelDbCleaner) markRecentlyUsed(ctx context.Context, key []byte) {
   340  	c.getCache().Add(c.cacheKey(key), true)
   341  	c.attemptClean(ctx)
   342  }
   343  
   344  func (c *levelDbCleaner) removeRecentlyUsed(ctx context.Context, key []byte) {
   345  	c.getCache().Remove(c.cacheKey(key))
   346  	c.attemptClean(ctx)
   347  }