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

     1  package avatars
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/keybase/client/go/libkb"
     8  	"github.com/keybase/client/go/lru"
     9  	"github.com/keybase/client/go/protocol/keybase1"
    10  )
    11  
    12  type URLCachingSource struct {
    13  	diskLRU        *lru.DiskLRU
    14  	staleThreshold time.Duration
    15  	simpleSource   *SimpleSource
    16  
    17  	// testing only
    18  	staleFetchCh chan struct{}
    19  }
    20  
    21  var _ libkb.AvatarLoaderSource = (*URLCachingSource)(nil)
    22  
    23  func NewURLCachingSource(staleThreshold time.Duration, size int) *URLCachingSource {
    24  	return &URLCachingSource{
    25  		diskLRU:        lru.NewDiskLRU("avatarurls", 1, size),
    26  		staleThreshold: staleThreshold,
    27  		simpleSource:   NewSimpleSource(),
    28  	}
    29  }
    30  
    31  func (c *URLCachingSource) StartBackgroundTasks(m libkb.MetaContext) {
    32  	go c.monitorAppState(m)
    33  }
    34  
    35  func (c *URLCachingSource) StopBackgroundTasks(m libkb.MetaContext) {
    36  	c.diskLRU.Flush(m.Ctx(), m.G())
    37  }
    38  
    39  func (c *URLCachingSource) debug(m libkb.MetaContext, msg string, args ...interface{}) {
    40  	m.Debug("Avatars.URLCachingSource: %s", fmt.Sprintf(msg, args...))
    41  }
    42  
    43  func (c *URLCachingSource) avatarKey(name string, format keybase1.AvatarFormat) string {
    44  	return fmt.Sprintf("%s:%s", name, format.String())
    45  }
    46  
    47  func (c *URLCachingSource) isStale(m libkb.MetaContext, item lru.DiskLRUEntry) bool {
    48  	return m.G().GetClock().Now().Sub(item.Ctime) > c.staleThreshold
    49  }
    50  
    51  func (c *URLCachingSource) monitorAppState(m libkb.MetaContext) {
    52  	c.debug(m, "monitorAppState: starting up")
    53  	state := keybase1.MobileAppState_FOREGROUND
    54  	for {
    55  		state = <-m.G().MobileAppState.NextUpdate(&state)
    56  		if state == keybase1.MobileAppState_BACKGROUND {
    57  			c.debug(m, "monitorAppState: backgrounded")
    58  			c.diskLRU.Flush(m.Ctx(), m.G())
    59  		}
    60  	}
    61  }
    62  
    63  func (c *URLCachingSource) specLoad(m libkb.MetaContext, names []string, formats []keybase1.AvatarFormat) (res avatarLoadSpec, err error) {
    64  	for _, name := range names {
    65  		for _, format := range formats {
    66  			key := c.avatarKey(name, format)
    67  			found, entry, err := c.diskLRU.Get(m.Ctx(), m.G(), key)
    68  			if err != nil {
    69  				return res, err
    70  			}
    71  			lp := avatarLoadPair{
    72  				name:   name,
    73  				format: format,
    74  			}
    75  			if found {
    76  				lp.path = entry.Value.(string)
    77  				if c.isStale(m, entry) {
    78  					res.stales = append(res.stales, lp)
    79  				} else {
    80  					res.hits = append(res.hits, lp)
    81  				}
    82  			} else {
    83  				res.misses = append(res.misses, lp)
    84  			}
    85  		}
    86  	}
    87  	return res, nil
    88  }
    89  
    90  func (c *URLCachingSource) mergeRes(res *keybase1.LoadAvatarsRes, m keybase1.LoadAvatarsRes) {
    91  	for username, rec := range m.Picmap {
    92  		for format, url := range rec {
    93  			res.Picmap[username][format] = url
    94  		}
    95  	}
    96  }
    97  
    98  func (c *URLCachingSource) commitURLs(m libkb.MetaContext, res keybase1.LoadAvatarsRes) {
    99  	for name, rec := range res.Picmap {
   100  		for format, url := range rec {
   101  			if _, err := c.diskLRU.Put(m.Ctx(), m.G(), c.avatarKey(name, format), url); err != nil {
   102  				c.debug(m, "commitURLs: failed to save URL: url: %s err: %s", url, err)
   103  			}
   104  		}
   105  	}
   106  }
   107  
   108  func (c *URLCachingSource) loadNames(m libkb.MetaContext, names []string, formats []keybase1.AvatarFormat,
   109  	remoteFetch func(libkb.MetaContext, []string, []keybase1.AvatarFormat) (keybase1.LoadAvatarsRes, error)) (res keybase1.LoadAvatarsRes, err error) {
   110  	loadSpec, err := c.specLoad(m, names, formats)
   111  	if err != nil {
   112  		return res, err
   113  	}
   114  	c.debug(m, "loadNames: hits: %d stales: %d misses: %d", len(loadSpec.hits), len(loadSpec.stales),
   115  		len(loadSpec.misses))
   116  
   117  	// Fill in the hits
   118  	allocRes(&res, names)
   119  	for _, hit := range loadSpec.hits {
   120  		res.Picmap[hit.name][hit.format] = keybase1.MakeAvatarURL(hit.path)
   121  	}
   122  	// Fill in stales
   123  	for _, stale := range loadSpec.stales {
   124  		res.Picmap[stale.name][stale.format] = keybase1.MakeAvatarURL(stale.path)
   125  	}
   126  
   127  	// Go get the misses
   128  	missNames, missFormats := loadSpec.missDetails()
   129  	if len(missNames) > 0 {
   130  		loadRes, err := remoteFetch(m, missNames, missFormats)
   131  		if err == nil {
   132  			c.commitURLs(m, loadRes)
   133  			c.mergeRes(&res, loadRes)
   134  		} else {
   135  			c.debug(m, "loadNames: failed to load server miss reqs: %s", err)
   136  		}
   137  	}
   138  	// Spawn off a goroutine to reload stales
   139  	staleNames, staleFormats := loadSpec.staleDetails()
   140  	if len(staleNames) > 0 {
   141  		go func() {
   142  			m = m.BackgroundWithLogTags()
   143  			c.debug(m, "loadNames: spawning stale background load: names: %d",
   144  				len(staleNames))
   145  			loadRes, err := remoteFetch(m, staleNames, staleFormats)
   146  			if err != nil {
   147  				c.debug(m, "loadNames: failed to load server stale reqs: %s", err)
   148  			} else {
   149  				c.commitURLs(m, loadRes)
   150  			}
   151  			if c.staleFetchCh != nil {
   152  				c.staleFetchCh <- struct{}{}
   153  			}
   154  		}()
   155  	}
   156  	return res, nil
   157  }
   158  
   159  func (c *URLCachingSource) clearName(m libkb.MetaContext, name string, formats []keybase1.AvatarFormat) (err error) {
   160  	for _, format := range formats {
   161  		key := c.avatarKey(name, format)
   162  		if err := c.diskLRU.Remove(m.Ctx(), m.G(), key); err != nil {
   163  			return err
   164  		}
   165  	}
   166  	return nil
   167  }
   168  
   169  func (c *URLCachingSource) LoadUsers(m libkb.MetaContext, usernames []string, formats []keybase1.AvatarFormat) (res keybase1.LoadAvatarsRes, err error) {
   170  	defer m.Trace("URLCachingSource.LoadUsers", &err)()
   171  	return c.loadNames(m, usernames, formats, c.simpleSource.LoadUsers)
   172  }
   173  
   174  func (c *URLCachingSource) LoadTeams(m libkb.MetaContext, teams []string, formats []keybase1.AvatarFormat) (res keybase1.LoadAvatarsRes, err error) {
   175  	defer m.Trace("URLCachingSource.LoadTeams", &err)()
   176  	return c.loadNames(m, teams, formats, c.simpleSource.LoadTeams)
   177  }
   178  
   179  func (c *URLCachingSource) ClearCacheForName(m libkb.MetaContext, name string, formats []keybase1.AvatarFormat) (err error) {
   180  	defer m.Trace(fmt.Sprintf("URLCachingSource.ClearCacheForUser(%s,%v)", name, formats), &err)()
   181  	return c.clearName(m, name, formats)
   182  }
   183  
   184  func (c *URLCachingSource) OnDbNuke(m libkb.MetaContext) error { return nil }