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 }