/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/contacts/cache.go
299 строк
10 KB
zoom-ua
gofumpt (#28710)
17 дек 2025, 17:16
Не верифицирован
17 дек 2025, 17:16
cda510b
Код
Авторство
О чём код?
// Copyright 2019 Keybase, Inc. All rights reserved. Use of // this source code is governed by the included BSD license. package contacts import ( "context" "fmt" "sync" "time" "github.com/keybase/client/go/encrypteddb" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/keybase1" ) // ContactCacheStore is used by CachedContactsProvider to store contact cache // encrypted with device key. type ContactCacheStore struct { encryptedDB *encrypteddb.EncryptedDB } func (s *ContactCacheStore) dbKey(uid keybase1.UID) libkb.DbKey { return libkb.DbKey{ Typ: libkb.DBContactResolution, Key: fmt.Sprintf("%v", uid), } } // NewContactCacheStore creates new ContactCacheStore for global context. The // store is used to securely store cached contact resolutions. func NewContactCacheStore(g *libkb.GlobalContext) *ContactCacheStore { keyFn := func(ctx context.Context) ([32]byte, error) { return encrypteddb.GetSecretBoxKey(ctx, g, libkb.EncryptionReasonContactsLocalStorage, "encrypting contact resolution cache") } dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { return g.LocalDb } return &ContactCacheStore{ encryptedDB: encrypteddb.New(g, dbFn, keyFn), } } type CachedContactsProvider struct { lock sync.Mutex Provider ContactsProvider Store *ContactCacheStore } var _ ContactsProvider = (*CachedContactsProvider)(nil) type cachedLookupResult struct { ContactLookupResult Resolved bool ExpiresAt time.Time } type lookupResultCache struct { Lookups map[ContactLookupKey]cachedLookupResult Token Token Version struct { Major int Minor int } } func newLookupResultCache() (ret lookupResultCache) { ret = lookupResultCache{ Lookups: make(map[ContactLookupKey]cachedLookupResult), } ret.Version.Major = cacheCurrentMajorVersion ret.Version.Minor = cacheCurrentMinorVersion return ret } const ( cacheCurrentMajorVersion = 2 cacheCurrentMinorVersion = 0 ) func cachedResultFromLookupResult(v ContactLookupResult, expires time.Time) cachedLookupResult { return cachedLookupResult{ ContactLookupResult: v, Resolved: true, ExpiresAt: expires, } } // Time after we throw away the result and not return it anymore. When a cached // entry expires, we will try to update it, but if we fail to do so, we will // still return it - so user does not lose all their cache if they happen to be // offline after expiration time. But if a cached entry has not been refreshed // for duration of server provided expiration time plus cacheEvictionTime, it's // discarded entirely. const cacheEvictionTime = 45 * 24 * time.Hour // approx 45 days func (c cachedLookupResult) getEvictionTime() time.Time { return c.ExpiresAt.Add(cacheEvictionTime) } func (c *lookupResultCache) findFreshOrSetEmpty(mctx libkb.MetaContext, key ContactLookupKey) (res cachedLookupResult, stale bool, found bool) { now := mctx.G().Clock().Now() res, found = c.Lookups[key] if !found || now.After(res.getEvictionTime()) { // Pre-insert to the cache. If Provider.LookupAll does not find // these, they will stay in the cache as unresolved, otherwise they // are overwritten. // Caller is supposed to set proper ExpiresAt value. res = cachedLookupResult{Resolved: false, ExpiresAt: now} c.Lookups[key] = res return res, false, false } return res, now.After(res.ExpiresAt), true } func (c *lookupResultCache) cleanup(mctx libkb.MetaContext) { now := mctx.G().Clock().Now() for key, val := range c.Lookups { if now.After(val.getEvictionTime()) { delete(c.Lookups, key) } } } func (s *ContactCacheStore) getCache(mctx libkb.MetaContext) (obj lookupResultCache, created bool) { var conCache lookupResultCache var createCache bool cacheKey := s.dbKey(mctx.CurrentUID()) found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &conCache) switch { case err != nil: mctx.Warning("Unable to pull contact lookup cache: %s", err) createCache = true case !found: mctx.Debug("No contact lookup cache found, creating new cache object") createCache = true case conCache.Version.Major != cacheCurrentMajorVersion: mctx.Debug("Found contact cache object but major version is %d (need %d)", conCache.Version.Major, cacheCurrentMajorVersion) createCache = true } // NOTE: If we ever have a cache change that keeps major version same but // increases minor version, do the object upgrade here. if createCache { conCache = newLookupResultCache() } return conCache, createCache } func (s *ContactCacheStore) putCache(mctx libkb.MetaContext, cacheObj lookupResultCache) error { cacheKey := s.dbKey(mctx.CurrentUID()) return s.encryptedDB.Put(mctx.Ctx(), cacheKey, cacheObj) } func (s *ContactCacheStore) ClearCache(mctx libkb.MetaContext) error { cacheKey := s.dbKey(mctx.CurrentUID()) return s.encryptedDB.Delete(mctx.Ctx(), cacheKey) } func (c *CachedContactsProvider) LookupAllWithToken(mctx libkb.MetaContext, emails []keybase1.EmailAddress, numbers []keybase1.RawPhoneNumber, _ Token, ) (res ContactLookupResults, err error) { return c.LookupAll(mctx, emails, numbers) } func (c *CachedContactsProvider) LookupAll(mctx libkb.MetaContext, emails []keybase1.EmailAddress, numbers []keybase1.RawPhoneNumber, ) (res ContactLookupResults, err error) { defer mctx.Trace(fmt.Sprintf("CachedContactsProvider#LookupAll(len=%d)", len(emails)+len(numbers)), nil)() res = NewContactLookupResults() if len(emails)+len(numbers) == 0 { return res, nil } // This is a rather long-lived lock, because normally it will be held // through the entire duration of the lookup, but: // - We don't expect this to be called concurrently, or repeatedly, without // user's interaction. // - We want to avoid looking up the same assertion multiple times (burning // through the rate limit), while keeping the locking strategy simple. c.lock.Lock() defer c.lock.Unlock() conCache, _ := c.Store.getCache(mctx) var remainingEmails []keybase1.EmailAddress var remainingNumbers []keybase1.RawPhoneNumber mctx.Debug("Populating results from cache") // List of keys of new or stale cache entries, to set ExpireAt value after // we do parent provider LookupAll call. newCacheEntries := make([]ContactLookupKey, 0, len(remainingEmails)+len(remainingNumbers)) for _, email := range emails { key := MakeEmailLookupKey(email) cache, stale, found := conCache.findFreshOrSetEmpty(mctx, key) if found && cache.Resolved { // Store result even if stale, but may be overwritten by API query later. res.Results[key] = cache.ContactLookupResult } if !found || stale { remainingEmails = append(remainingEmails, email) newCacheEntries = append(newCacheEntries, key) } } for _, number := range numbers { key := MakePhoneLookupKey(number) cache, stale, found := conCache.findFreshOrSetEmpty(mctx, key) if found && cache.Resolved { // Store result even if stale, but may be overwritten by API query later. res.Results[key] = cache.ContactLookupResult } if !found || stale { remainingNumbers = append(remainingNumbers, number) newCacheEntries = append(newCacheEntries, key) } } mctx.Debug("After checking cache, %d emails and %d numbers left to be looked up", len(remainingEmails), len(remainingNumbers)) if len(remainingEmails)+len(remainingNumbers) > 0 { apiRes, err := c.Provider.LookupAllWithToken(mctx, remainingEmails, remainingNumbers, conCache.Token) if err == nil { conCache.Token = apiRes.Token now := mctx.G().Clock().Now() expiresAt := now.Add(apiRes.ResolvedFreshness) for k, v := range apiRes.Results { res.Results[k] = v conCache.Lookups[k] = cachedResultFromLookupResult(v, expiresAt) } // Loop through entries that we asked for and find these we did not get // resolutions for. Set ExpiresAt now that we know UnresolvedFreshness. unresolvedExpiresAt := now.Add(apiRes.UnresolvedFreshness) for _, key := range newCacheEntries { val := conCache.Lookups[key] if !val.Resolved { val.ExpiresAt = unresolvedExpiresAt conCache.Lookups[key] = val } } } else { mctx.Warning("Unable to call Provider.LookupAll, returning only cached results: %s", err) } conCache.cleanup(mctx) cerr := c.Store.putCache(mctx, conCache) if cerr != nil { mctx.Warning("Unable to update cache: %s", cerr) } } return res, nil } func (c *CachedContactsProvider) FindUsernames(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]ContactUsernameAndFullName, error) { return c.Provider.FindUsernames(mctx, uids) } func (c *CachedContactsProvider) FindFollowing(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]bool, error) { return c.Provider.FindFollowing(mctx, uids) } func (c *CachedContactsProvider) FindServiceMaps(mctx libkb.MetaContext, uids []keybase1.UID) (map[keybase1.UID]libkb.UserServiceSummary, error) { return c.Provider.FindServiceMaps(mctx, uids) } // RemoveContactsCachePhoneEntry removes cached lookup for phone number. func (s *ContactCacheStore) RemoveContactsCacheEntries(mctx libkb.MetaContext, phone *keybase1.PhoneNumber, email *keybase1.EmailAddress, ) { // TODO: Use a phoneNumber | email variant instead of two pointers. cacheObj, created := s.getCache(mctx) if created { // There was no cache. return } if phone != nil { // TODO: this type conversion shouldn't have to be here, // since this cache should take `PhoneNumber`s. delete(cacheObj.Lookups, MakePhoneLookupKey(keybase1.RawPhoneNumber(*phone))) mctx.Debug("ContactCacheStore: Removing phone number %q from lookup cache", *phone) } if email != nil { delete(cacheObj.Lookups, MakeEmailLookupKey(*email)) mctx.Debug("ContactCacheStore: Removing email %q from lookup cache", *email) } err := s.putCache(mctx, cacheObj) if err != nil { mctx.Warning("ContactCacheStore: Unable to update cache: %s", err) } }