github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/favorites.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libkbfs
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/keybase/client/go/kbfs/data"
    16  	"github.com/keybase/client/go/kbfs/favorites"
    17  	"github.com/keybase/client/go/kbfs/kbfssync"
    18  	"github.com/keybase/client/go/kbfs/ldbutils"
    19  	"github.com/keybase/client/go/kbfs/tlf"
    20  	"github.com/keybase/client/go/libkb"
    21  	"github.com/keybase/client/go/logger"
    22  	"github.com/keybase/client/go/protocol/keybase1"
    23  	"github.com/syndtr/goleveldb/leveldb"
    24  	"github.com/syndtr/goleveldb/leveldb/storage"
    25  	"golang.org/x/net/context"
    26  )
    27  
    28  const (
    29  	disableFavoritesEnvVar           = "KEYBASE_DISABLE_FAVORITES"
    30  	favoritesCacheExpirationTime     = time.Hour * 24 * 7 // one week
    31  	kbfsFavoritesCacheSubfolder      = "kbfs_favorites"
    32  	favoritesDiskCacheFilename       = "kbfsFavorites.leveldb"
    33  	favoritesDiskCacheVersion        = 2
    34  	favoritesDiskCacheStorageVersion = 1
    35  	// How long to block on favorites refresh when cache is expired (e.g.,
    36  	// on startup). Reasonably low in case we're offline.
    37  	favoritesServerTimeoutWhenCacheExpired = 500 * time.Millisecond
    38  	favoritesBackgroundRefreshTimeout      = 15 * time.Second
    39  	defaultFavoritesBufferedReqInterval    = 5 * time.Second
    40  )
    41  
    42  var errNoFavoritesCache = errors.New("disk favorites cache not present")
    43  
    44  type errIncorrectFavoritesCacheVersion struct {
    45  	cache   string
    46  	version int
    47  }
    48  
    49  func (e errIncorrectFavoritesCacheVersion) Error() string {
    50  	return fmt.Sprintf("decoding of %s favorites cache failed: version was %d",
    51  		e.cache, e.version)
    52  }
    53  
    54  // favReq represents a request to access the logged-in user's
    55  // favorites list.  A single request can do one or more of the
    56  // following: refresh the current cached list, add a favorite, remove
    57  // a favorite, and get all the favorites.  When the request is done,
    58  // the resulting error (or nil) is sent over the done channel.  The
    59  // given ctx is used for all network operations.
    60  type favReq struct {
    61  	// Request types
    62  	clear              bool
    63  	refresh            bool
    64  	refreshID          tlf.ID
    65  	buffered           bool
    66  	toAdd              []favorites.ToAdd
    67  	toDel              []favorites.Folder
    68  	toGet              *favorites.Folder
    69  	folderWithFavFlags chan<- *keybase1.FolderWithFavFlags
    70  	favs               chan<- []favorites.Folder
    71  	favsAll            chan<- keybase1.FavoritesResult
    72  	homeTLFInfo        *homeTLFInfo
    73  
    74  	// For asynchronous refreshes, pass in the Favorites from the server here
    75  	favResult *keybase1.FavoritesResult
    76  
    77  	// Closed when the request is done.
    78  	done chan struct{}
    79  	// Set before done is closed
    80  	err error
    81  
    82  	// Context
    83  	ctx context.Context
    84  }
    85  
    86  type homeTLFInfo struct {
    87  	PublicTeamID  keybase1.TeamID
    88  	PrivateTeamID keybase1.TeamID
    89  }
    90  
    91  // Favorites manages a user's favorite list.
    92  type Favorites struct {
    93  	config           Config
    94  	disabled         bool
    95  	log              logger.Logger
    96  	bufferedInterval time.Duration
    97  
    98  	// homeTLFInfo stores the IDs for the logged-in user's home TLFs
    99  	homeTLFInfo homeTLFInfo
   100  
   101  	// Channel for interacting with the favorites cache
   102  	reqChan         chan *favReq
   103  	bufferedReqChan chan *favReq
   104  	// Channel that is full when there is already a refresh queued
   105  	refreshWaiting chan struct{}
   106  
   107  	wg         kbfssync.RepeatedWaitGroup
   108  	bufferedWg kbfssync.RepeatedWaitGroup
   109  	loopWG     kbfssync.RepeatedWaitGroup
   110  
   111  	// cache tracks the favorites for this user, that we know about.
   112  	// It may not be consistent with the server's view of the user's
   113  	// favorites list, if other devices have modified the list since
   114  	// the last refresh and this device is offline.
   115  	// When another device modifies the favorites [or new or ignored] list,
   116  	// the server will try to alert the other devices to refresh.
   117  	favCache        map[favorites.Folder]favorites.Data
   118  	newCache        map[favorites.Folder]favorites.Data
   119  	ignoredCache    map[favorites.Folder]favorites.Data
   120  	cacheExpireTime time.Time
   121  
   122  	diskCache *ldbutils.LevelDb
   123  
   124  	inFlightLock sync.Mutex
   125  	inFlightAdds map[favorites.Folder]*favReq
   126  
   127  	idLock        sync.Mutex
   128  	lastRefreshID tlf.ID
   129  
   130  	shutdownChan chan struct{}
   131  	muShutdown   sync.RWMutex
   132  	shutdown     bool
   133  }
   134  
   135  func newFavoritesWithChan(config Config, reqChan chan *favReq) *Favorites {
   136  	disableVal := strings.ToLower(os.Getenv(disableFavoritesEnvVar))
   137  	log := config.MakeLogger("FAV")
   138  	if len(disableVal) > 0 && disableVal != "0" && disableVal != "false" &&
   139  		disableVal != "no" {
   140  		log.CDebugf(
   141  			context.TODO(), "Disable favorites due to env var %s=%s",
   142  			disableFavoritesEnvVar, disableVal)
   143  		return &Favorites{
   144  			config:   config,
   145  			disabled: true,
   146  			log:      log,
   147  		}
   148  	}
   149  
   150  	f := &Favorites{
   151  		config:           config,
   152  		reqChan:          reqChan,
   153  		bufferedReqChan:  make(chan *favReq, 1),
   154  		refreshWaiting:   make(chan struct{}, 1),
   155  		inFlightAdds:     make(map[favorites.Folder]*favReq),
   156  		log:              log,
   157  		bufferedInterval: defaultFavoritesBufferedReqInterval,
   158  		shutdownChan:     make(chan struct{}),
   159  	}
   160  
   161  	return f
   162  }
   163  
   164  // NewFavorites constructs a new Favorites instance.
   165  func NewFavorites(config Config) *Favorites {
   166  	return newFavoritesWithChan(config, make(chan *favReq, 100))
   167  }
   168  
   169  type favoritesCacheForDisk struct {
   170  	Version      int
   171  	FavCache     map[favorites.Folder]favorites.Data
   172  	NewCache     map[favorites.Folder]favorites.Data
   173  	IgnoredCache map[favorites.Folder]favorites.Data
   174  }
   175  type favoritesCacheEncryptedForDisk struct {
   176  	Version        int
   177  	EncryptedCache []byte
   178  }
   179  
   180  func (f *Favorites) readCacheFromDisk(ctx context.Context) error {
   181  	// Read the encrypted cache from disk
   182  	var db *ldbutils.LevelDb
   183  	var err error
   184  	if f.config.IsTestMode() {
   185  		db, err = ldbutils.OpenLevelDb(storage.NewMemStorage(), f.config.Mode())
   186  	} else {
   187  		db, err = ldbutils.OpenVersionedLevelDb(f.log, f.config.StorageRoot(),
   188  			kbfsFavoritesCacheSubfolder, favoritesDiskCacheStorageVersion,
   189  			favoritesDiskCacheFilename, f.config.Mode())
   190  	}
   191  	if err != nil {
   192  		return err
   193  	}
   194  	f.diskCache = db
   195  	session, err := f.config.KBPKI().GetCurrentSession(ctx)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	user := []byte(string(session.UID))
   200  	data, err := db.Get(user, nil)
   201  	if err == leveldb.ErrNotFound {
   202  		f.log.CInfof(ctx, "No favorites cache found for user %v", user)
   203  		return nil
   204  	} else if err != nil {
   205  		return err
   206  	}
   207  
   208  	// decode the data from the file and ensure its version is correct
   209  	var decodedData favoritesCacheEncryptedForDisk
   210  	err = f.config.Codec().Decode(data, &decodedData)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	if decodedData.Version != favoritesDiskCacheStorageVersion {
   215  		return errIncorrectFavoritesCacheVersion{cache: "serialized",
   216  			version: decodedData.Version}
   217  	}
   218  
   219  	// Send the data to the service to be decrypted
   220  	decryptedData, err := f.config.KeybaseService().DecryptFavorites(ctx,
   221  		decodedData.EncryptedCache)
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	// Decode the data into the a map
   227  	var cacheDecoded favoritesCacheForDisk
   228  	err = f.config.Codec().Decode(decryptedData, &cacheDecoded)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	if cacheDecoded.Version != favoritesDiskCacheVersion {
   233  		return errIncorrectFavoritesCacheVersion{cache: "encrypted",
   234  			version: decodedData.Version}
   235  	}
   236  
   237  	f.favCache = cacheDecoded.FavCache
   238  	f.newCache = cacheDecoded.NewCache
   239  	f.ignoredCache = cacheDecoded.IgnoredCache
   240  	return nil
   241  }
   242  
   243  func (f *Favorites) writeCacheToDisk(ctx context.Context) error {
   244  	if f.diskCache == nil {
   245  		return errNoFavoritesCache
   246  	}
   247  	// Encode the cache map into a byte buffer
   248  	cacheForDisk := favoritesCacheForDisk{
   249  		FavCache:     f.favCache,
   250  		NewCache:     f.newCache,
   251  		IgnoredCache: f.ignoredCache,
   252  		Version:      favoritesDiskCacheVersion,
   253  	}
   254  	cacheSerialized, err := f.config.Codec().Encode(cacheForDisk)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	// Send the byte buffer to the service for encryption
   260  	data, err := f.config.KeybaseService().EncryptFavorites(ctx,
   261  		cacheSerialized)
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	// Encode the encrypted data in a versioned struct before writing it to
   267  	// the LevelDb.
   268  	cacheEncryptedForDisk := favoritesCacheEncryptedForDisk{
   269  		EncryptedCache: data,
   270  		Version:        favoritesDiskCacheStorageVersion,
   271  	}
   272  	encodedData, err := f.config.Codec().Encode(cacheEncryptedForDisk)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	// Write the encrypted cache to disk
   278  	session, err := f.config.KBPKI().GetCurrentSession(ctx)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	user := []byte(string(session.UID))
   283  	return f.diskCache.Put(user, encodedData, nil)
   284  }
   285  
   286  // InitForTest starts the Favorites cache's internal processing loop without
   287  // loading cached favorites from disk.
   288  func (f *Favorites) InitForTest() {
   289  	if f.disabled {
   290  		return
   291  	}
   292  	go f.loop()
   293  }
   294  
   295  // Initialize loads the favorites cache from disk and starts listening for
   296  // requests asynchronously.
   297  func (f *Favorites) Initialize(ctx context.Context) {
   298  	if f.disabled {
   299  		return
   300  	}
   301  	// load cache from disk
   302  	err := f.readCacheFromDisk(ctx)
   303  	if err != nil {
   304  		f.log.CWarningf(
   305  			ctx, "Failed to read cached favorites from disk: %v", err)
   306  	}
   307  
   308  	// launch background loop
   309  	go f.loop()
   310  }
   311  
   312  func (f *Favorites) closeReq(req *favReq, err error) {
   313  	f.inFlightLock.Lock()
   314  	defer f.inFlightLock.Unlock()
   315  	req.err = err
   316  	close(req.done)
   317  	for _, fav := range req.toAdd {
   318  		delete(f.inFlightAdds, fav.Folder)
   319  	}
   320  }
   321  
   322  func (f *Favorites) crossCheckWithEditHistory() {
   323  	// NOTE: Ideally we would wait until all edit activity processing
   324  	// had completed in the FBO before we do these checks, but I think
   325  	// in practice when the mtime changes, it'll be the edit activity
   326  	// processing that actually kicks off these favorites activity,
   327  	// and so that particular race won't be an issue.  If we see
   328  	// weirdness here though, it might be worth revisiting that
   329  	// assumption.
   330  
   331  	// The mtime attached to the favorites data returned by the API
   332  	// server is updated both on git activity, background collection,
   333  	// and pure deletion ops, none of which add new interesting
   334  	// content to the TLF.  So, fix the favorite times to be the
   335  	// latest known edit history times, if possible.  If not possible,
   336  	// that means the TLF is definitely not included in the latest
   337  	// list of TLF edit activity; so set these to be lower than the
   338  	// minimum known time, if they're not already.
   339  	var minTime keybase1.Time
   340  	uh := f.config.UserHistory()
   341  	tlfsWithNoHistory := make(map[favorites.Folder]favorites.Data)
   342  	for fav, data := range f.favCache {
   343  		h := uh.GetTlfHistory(tlf.CanonicalName(fav.Name), fav.Type)
   344  		if h.ServerTime == 0 {
   345  			if data.TlfMtime != nil {
   346  				tlfsWithNoHistory[fav] = data
   347  			}
   348  			continue
   349  		}
   350  		if minTime == 0 || h.ServerTime < minTime {
   351  			minTime = h.ServerTime
   352  		}
   353  		if data.TlfMtime == nil || *data.TlfMtime > h.ServerTime {
   354  			t := h.ServerTime
   355  			data.TlfMtime = &t
   356  			f.favCache[fav] = data
   357  		}
   358  	}
   359  
   360  	// Make sure all TLFs that aren't in the recent edit history get a
   361  	// timestamp that's smaller than the minimum time in the edit
   362  	// history.
   363  	if minTime > 0 {
   364  		for fav, data := range tlfsWithNoHistory {
   365  			if *data.TlfMtime > minTime {
   366  				t := minTime - 1
   367  				data.TlfMtime = &t
   368  				f.favCache[fav] = data
   369  			}
   370  		}
   371  	}
   372  }
   373  
   374  // sendChangesToEditHistory notes any deleted favorites and removes them
   375  // from this user's kbfsedits.UserHistory.
   376  func (f *Favorites) sendChangesToEditHistory(oldCache map[favorites.Folder]favorites.Data) (changed bool) {
   377  	for oldFav := range oldCache {
   378  		if _, present := f.favCache[oldFav]; !present {
   379  			f.config.UserHistory().ClearTLF(tlf.CanonicalName(oldFav.Name),
   380  				oldFav.Type)
   381  			changed = true
   382  		}
   383  	}
   384  	for newFav, newFavData := range f.favCache {
   385  		oldFavData, present := oldCache[newFav]
   386  		if !present {
   387  			f.config.KBFSOps().RefreshEditHistory(newFav)
   388  			changed = true
   389  		} else if newFavData.TlfMtime != nil &&
   390  			(oldFavData.TlfMtime == nil ||
   391  				(*newFavData.TlfMtime > *oldFavData.TlfMtime)) {
   392  			changed = true
   393  		}
   394  	}
   395  
   396  	return changed
   397  }
   398  
   399  func favoriteToFolder(fav favorites.Folder, data favorites.Data) keybase1.Folder {
   400  	return keybase1.Folder{
   401  		Name:         fav.Name,
   402  		Private:      data.Private,
   403  		Created:      false,
   404  		FolderType:   data.FolderType,
   405  		TeamID:       data.TeamID,
   406  		ResetMembers: data.ResetMembers,
   407  		Mtime:        data.TlfMtime,
   408  	}
   409  }
   410  
   411  func (f *Favorites) doIDRefresh(id tlf.ID) bool {
   412  	f.idLock.Lock()
   413  	defer f.idLock.Unlock()
   414  
   415  	if f.lastRefreshID == id {
   416  		return false
   417  	}
   418  	f.lastRefreshID = id
   419  	return true
   420  }
   421  
   422  func (f *Favorites) clearLastRefreshID() {
   423  	f.idLock.Lock()
   424  	defer f.idLock.Unlock()
   425  	f.lastRefreshID = tlf.NullID
   426  }
   427  
   428  func (f *Favorites) handleReq(req *favReq) (err error) {
   429  	defer f.wg.Done()
   430  
   431  	changed := false
   432  	defer func() {
   433  		f.closeReq(req, err)
   434  		if changed {
   435  			f.config.SubscriptionManagerPublisher().PublishChange(keybase1.SubscriptionTopic_FAVORITES)
   436  			f.config.Reporter().NotifyFavoritesChanged(req.ctx)
   437  		}
   438  	}()
   439  
   440  	if req.refresh && !req.buffered {
   441  		<-f.refreshWaiting
   442  	}
   443  
   444  	if req.refresh && req.refreshID != tlf.NullID &&
   445  		!f.doIDRefresh(req.refreshID) {
   446  		return nil
   447  	}
   448  
   449  	kbpki := f.config.KBPKI()
   450  	// Fetch a new list if:
   451  	//  (1) The user asked us to refresh
   452  	//  (2) We haven't fetched it before
   453  	//  (3) It's stale
   454  	//
   455  	// If just (3), use a short timeout so we can return the correct result
   456  	// quickly when offline.
   457  	needFetch := (req.refresh || f.favCache == nil) && !req.clear
   458  	wantFetch := f.config.Clock().Now().After(f.cacheExpireTime) && !req.clear
   459  
   460  	for _, fav := range req.toAdd {
   461  		// This check for adds is critical and we should definitely leave it
   462  		// in. We've had issues in the past with spamming the API server with
   463  		// adding the same favorite multiple times. We don't have the same
   464  		// problem with deletes, because after the user deletes it, they aren't
   465  		// accessing the folder again. But with adds, we could be going through
   466  		// this code on basically every folder access. Favorite deletes from
   467  		// another device result in a notification to this device, so a race
   468  		// condition where we miss an "add" can't happen.
   469  		_, present := f.favCache[fav.Folder]
   470  		if !fav.Created && present {
   471  			continue
   472  		}
   473  		err := kbpki.FavoriteAdd(req.ctx, fav.ToKBFolderHandle())
   474  		if err != nil {
   475  			f.log.CDebugf(req.ctx,
   476  				"Failure adding favorite %v: %v", fav, err)
   477  			return err
   478  		}
   479  		needFetch = true
   480  		changed = true
   481  	}
   482  
   483  	for _, fav := range req.toDel {
   484  		// Since our cache isn't necessarily up-to-date, always delete
   485  		// the favorite.
   486  		folder := fav.ToKBFolderHandle(false)
   487  		err := kbpki.FavoriteDelete(req.ctx, folder)
   488  		if err != nil {
   489  			return err
   490  		}
   491  		f.config.UserHistory().ClearTLF(tlf.CanonicalName(fav.Name), fav.Type)
   492  		changed = true
   493  		// Simply delete here instead of triggering another list as an
   494  		// optimization because there's nothing additional we need from core.
   495  		delete(f.favCache, fav)
   496  	}
   497  
   498  	if needFetch || wantFetch {
   499  		getCtx := req.ctx
   500  		if !needFetch {
   501  			var cancel context.CancelFunc
   502  			getCtx, cancel = context.WithTimeout(req.ctx,
   503  				favoritesServerTimeoutWhenCacheExpired)
   504  			defer cancel()
   505  		}
   506  		// Load the cache from the server. This possibly already happened
   507  		// asynchronously and was included in the request.
   508  		var favResult keybase1.FavoritesResult
   509  		if req.favResult == nil {
   510  			favResult, err = kbpki.FavoriteList(getCtx)
   511  		} else {
   512  			favResult = *req.favResult
   513  		}
   514  		if err != nil {
   515  			if needFetch {
   516  				// if we're supposed to refresh the cache and it's not
   517  				// working, mark the current cache expired.
   518  				now := f.config.Clock().Now()
   519  				if now.Before(f.cacheExpireTime) {
   520  					f.cacheExpireTime = now
   521  				}
   522  				return err
   523  			}
   524  			// If we weren't explicitly asked to refresh, we can return possibly
   525  			// stale favorites rather than return nothing.
   526  			if err == context.DeadlineExceeded {
   527  				newCtx, _ := context.WithTimeout(context.Background(),
   528  					favoritesBackgroundRefreshTimeout)
   529  				go f.RefreshCache(newCtx, FavoritesRefreshModeBlocking)
   530  			}
   531  			f.log.CDebugf(req.ctx,
   532  				"Serving possibly stale favorites; new data could not be"+
   533  					" fetched: %v", err)
   534  		} else { // Successfully got new favorites from server.
   535  			if req.refreshID == tlf.NullID {
   536  				f.clearLastRefreshID()
   537  			}
   538  
   539  			session, sessionErr := kbpki.GetCurrentSession(req.ctx)
   540  			oldCache := f.favCache
   541  			f.newCache = make(map[favorites.Folder]favorites.Data)
   542  			f.favCache = make(map[favorites.Folder]favorites.Data)
   543  			f.ignoredCache = make(map[favorites.Folder]favorites.Data)
   544  			f.cacheExpireTime = libkb.ForceWallClock(f.config.Clock().Now()).Add(
   545  				favoritesCacheExpirationTime)
   546  			for _, folder := range favResult.FavoriteFolders {
   547  				f.favCache[*favorites.NewFolderFromProtocol(
   548  					folder)] = favorites.DataFrom(folder)
   549  				if sessionErr != nil && folder.Name == string(session.Name) {
   550  					if folder.Private {
   551  						f.homeTLFInfo.PrivateTeamID = *folder.TeamID
   552  					} else {
   553  						f.homeTLFInfo.PublicTeamID = *folder.TeamID
   554  					}
   555  				}
   556  			}
   557  			f.crossCheckWithEditHistory()
   558  			for _, folder := range favResult.IgnoredFolders {
   559  				f.ignoredCache[*favorites.NewFolderFromProtocol(
   560  					folder)] = favorites.DataFrom(folder)
   561  			}
   562  			for _, folder := range favResult.NewFolders {
   563  				f.newCache[*favorites.NewFolderFromProtocol(
   564  					folder)] = favorites.DataFrom(folder)
   565  			}
   566  			if sessionErr == nil {
   567  				// Add favorites for the current user, that cannot be
   568  				// deleted.  Only overwrite them (with a 0 mtime) if
   569  				// they weren't already part of the favorites list.
   570  				selfPriv := favorites.Folder{
   571  					Name: string(session.Name),
   572  					Type: tlf.Private,
   573  				}
   574  				if _, ok := f.favCache[selfPriv]; !ok {
   575  					f.favCache[selfPriv] = favorites.Data{
   576  						Name:       string(session.Name),
   577  						FolderType: tlf.Private.FolderType(),
   578  						TeamID:     &f.homeTLFInfo.PrivateTeamID,
   579  						Private:    true,
   580  					}
   581  				}
   582  				selfPub := favorites.Folder{
   583  					Name: string(session.Name),
   584  					Type: tlf.Public,
   585  				}
   586  				if _, ok := f.favCache[selfPub]; !ok {
   587  					f.favCache[selfPub] = favorites.Data{
   588  						Name:       string(session.Name),
   589  						FolderType: tlf.Public.FolderType(),
   590  						TeamID:     &f.homeTLFInfo.PublicTeamID,
   591  						Private:    false,
   592  					}
   593  				}
   594  				err = f.writeCacheToDisk(req.ctx)
   595  				if err != nil {
   596  					f.log.CWarningf(req.ctx,
   597  						"Could not write favorites to disk cache: %v", err)
   598  				}
   599  			}
   600  			if oldCache != nil {
   601  				changed = f.sendChangesToEditHistory(oldCache)
   602  			}
   603  		}
   604  	} else if req.clear {
   605  		f.favCache = nil
   606  		changed = true
   607  		return nil
   608  	}
   609  
   610  	if req.favs != nil {
   611  		favorites := make([]favorites.Folder, 0, len(f.favCache))
   612  		for fav := range f.favCache {
   613  			favorites = append(favorites, fav)
   614  		}
   615  		req.favs <- favorites
   616  	}
   617  
   618  	if req.favsAll != nil {
   619  		favFolders := make([]keybase1.Folder, 0, len(f.favCache))
   620  		newFolders := make([]keybase1.Folder, 0, len(f.newCache))
   621  		ignoredFolders := make([]keybase1.Folder, 0, len(f.ignoredCache))
   622  
   623  		for fav, data := range f.favCache {
   624  			favFolders = append(favFolders, favoriteToFolder(fav, data))
   625  		}
   626  		for fav, data := range f.newCache {
   627  			newFolders = append(newFolders, favoriteToFolder(fav, data))
   628  		}
   629  		for fav, data := range f.ignoredCache {
   630  			ignoredFolders = append(ignoredFolders, favoriteToFolder(fav, data))
   631  		}
   632  
   633  		req.favsAll <- keybase1.FavoritesResult{
   634  			NewFolders:      newFolders,
   635  			IgnoredFolders:  ignoredFolders,
   636  			FavoriteFolders: favFolders,
   637  		}
   638  	}
   639  
   640  	if req.folderWithFavFlags != nil && req.toGet != nil {
   641  		fav := *req.toGet
   642  		if data, ok := f.favCache[fav]; ok {
   643  			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
   644  				Folder:     favoriteToFolder(fav, data),
   645  				IsFavorite: true,
   646  			}
   647  		} else if data, ok := f.newCache[*req.toGet]; ok {
   648  			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
   649  				Folder: favoriteToFolder(fav, data),
   650  				IsNew:  true,
   651  			}
   652  		} else if data, ok := f.ignoredCache[*req.toGet]; ok {
   653  			req.folderWithFavFlags <- &keybase1.FolderWithFavFlags{
   654  				Folder:    favoriteToFolder(fav, data),
   655  				IsIgnored: true,
   656  			}
   657  		} else {
   658  			req.folderWithFavFlags <- nil
   659  		}
   660  	}
   661  
   662  	if req.homeTLFInfo != nil {
   663  		f.homeTLFInfo = *req.homeTLFInfo
   664  	}
   665  
   666  	return nil
   667  }
   668  
   669  func (f *Favorites) loop() {
   670  	f.loopWG.Add(1)
   671  	defer f.loopWG.Done()
   672  	bufferedTicker := time.NewTicker(f.bufferedInterval)
   673  	defer bufferedTicker.Stop()
   674  
   675  	for {
   676  		select {
   677  		case req, ok := <-f.reqChan:
   678  			if !ok {
   679  				return
   680  			}
   681  			err := f.handleReq(req)
   682  			if err != nil {
   683  				f.log.CDebugf(
   684  					context.TODO(), "Error handling request: %+v", err)
   685  			}
   686  		case <-bufferedTicker.C:
   687  			select {
   688  			case req, ok := <-f.bufferedReqChan:
   689  				if !ok {
   690  					// Still need to close out any regular requests.
   691  					continue
   692  				}
   693  				// Don't block the wait group on buffered requests
   694  				// until we're actually processing one.
   695  				f.wg.Add(1)
   696  				err := f.handleReq(req)
   697  				if err != nil {
   698  					f.log.CDebugf(
   699  						context.TODO(), "Error handling request: %+v", err)
   700  				}
   701  			default:
   702  			}
   703  		}
   704  	}
   705  }
   706  
   707  // Shutdown shuts down this Favorites instance.
   708  func (f *Favorites) Shutdown() error {
   709  	if f.disabled {
   710  		return nil
   711  	}
   712  
   713  	f.muShutdown.Lock()
   714  	defer f.muShutdown.Unlock()
   715  	f.shutdown = true
   716  	close(f.reqChan)
   717  	close(f.bufferedReqChan)
   718  	close(f.shutdownChan)
   719  	if f.diskCache != nil {
   720  		err := f.diskCache.Close()
   721  		if err != nil {
   722  			f.log.CWarningf(context.Background(),
   723  				"Could not close disk favorites cache: %v", err)
   724  		}
   725  	}
   726  	err := f.wg.Wait(context.Background())
   727  	if err != nil {
   728  		return err
   729  	}
   730  	return f.loopWG.Wait(context.Background())
   731  }
   732  
   733  func (f *Favorites) waitOnReq(ctx context.Context,
   734  	req *favReq) (retry bool, err error) {
   735  	select {
   736  	case <-ctx.Done():
   737  		return false, ctx.Err()
   738  	case <-req.done:
   739  		err = req.err
   740  		// If the request was canceled due to a context timeout that
   741  		// wasn't our own, try it again.
   742  		if err == context.Canceled || err == context.DeadlineExceeded {
   743  			select {
   744  			case <-ctx.Done():
   745  				return false, err
   746  			default:
   747  				return true, nil
   748  			}
   749  		}
   750  		return false, err
   751  	}
   752  }
   753  
   754  func (f *Favorites) sendReq(ctx context.Context, req *favReq) error {
   755  	f.wg.Add(1)
   756  	select {
   757  	case f.reqChan <- req:
   758  	case <-ctx.Done():
   759  		f.wg.Done()
   760  		err := ctx.Err()
   761  		f.closeReq(req, err)
   762  		return err
   763  	}
   764  	// With a direct sendReq call, we'll never have a shared request,
   765  	// so no need to check the retry status.
   766  	_, err := f.waitOnReq(ctx, req)
   767  	return err
   768  }
   769  
   770  func (f *Favorites) startOrJoinAddReq(
   771  	ctx context.Context, fav favorites.ToAdd) (req *favReq, doSend bool) {
   772  	f.inFlightLock.Lock()
   773  	defer f.inFlightLock.Unlock()
   774  	req, ok := f.inFlightAdds[fav.Folder]
   775  	if !ok {
   776  		req = &favReq{
   777  			ctx:   ctx,
   778  			toAdd: []favorites.ToAdd{fav},
   779  			done:  make(chan struct{}),
   780  		}
   781  		f.inFlightAdds[fav.Folder] = req
   782  		doSend = true
   783  	}
   784  	return req, doSend
   785  }
   786  
   787  // Add adds a favorite to your favorites list.
   788  func (f *Favorites) Add(ctx context.Context, fav favorites.ToAdd) error {
   789  	f.muShutdown.RLock()
   790  	defer f.muShutdown.RUnlock()
   791  
   792  	if f.disabled {
   793  		return nil
   794  	}
   795  	if f.shutdown {
   796  		return data.ShutdownHappenedError{}
   797  	}
   798  	doAdd := true
   799  	var err error
   800  	// Retry until we get an error that wasn't related to someone
   801  	// else's context being canceled.
   802  	for doAdd {
   803  		req, doSend := f.startOrJoinAddReq(ctx, fav)
   804  		if doSend {
   805  			return f.sendReq(ctx, req)
   806  		}
   807  		doAdd, err = f.waitOnReq(ctx, req)
   808  	}
   809  	return err
   810  }
   811  
   812  // AddAsync initiates a request to add this favorite to your favorites
   813  // list, if one is not already in flight, but it doesn't wait for the
   814  // result.  (It could block while kicking off the request, if lots of
   815  // different favorite operations are in flight.)  The given context is
   816  // used only for enqueuing the request on an internal queue, not for
   817  // any resulting I/O.
   818  func (f *Favorites) AddAsync(ctx context.Context, fav favorites.ToAdd) {
   819  	f.muShutdown.RLock()
   820  	defer f.muShutdown.RUnlock()
   821  
   822  	if f.disabled || f.shutdown {
   823  		return
   824  	}
   825  	// Use a fresh context, since we want the request to succeed even
   826  	// if the original context is canceled.
   827  	req, doSend := f.startOrJoinAddReq(context.Background(), fav)
   828  	if doSend {
   829  		f.wg.Add(1)
   830  		select {
   831  		case f.reqChan <- req:
   832  		case <-ctx.Done():
   833  			f.wg.Done()
   834  			err := ctx.Err()
   835  			f.closeReq(req, err)
   836  			return
   837  		}
   838  	}
   839  }
   840  
   841  // Delete deletes a favorite from the favorites list.  It is
   842  // idempotent.
   843  func (f *Favorites) Delete(ctx context.Context, fav favorites.Folder) error {
   844  	f.muShutdown.RLock()
   845  	defer f.muShutdown.RUnlock()
   846  
   847  	if f.disabled {
   848  		return nil
   849  	}
   850  	if f.shutdown {
   851  		return data.ShutdownHappenedError{}
   852  	}
   853  	return f.sendReq(ctx, &favReq{
   854  		ctx:   ctx,
   855  		toDel: []favorites.Folder{fav},
   856  		done:  make(chan struct{}),
   857  	})
   858  }
   859  
   860  // FavoritesRefreshMode controls how a favorites refresh happens.
   861  type FavoritesRefreshMode int
   862  
   863  const (
   864  	// FavoritesRefreshModeInMainFavoritesLoop means to refresh the favorites
   865  	// in the main loop, blocking any favorites requests after until the refresh
   866  	// is done.
   867  	FavoritesRefreshModeInMainFavoritesLoop = iota
   868  	// FavoritesRefreshModeBlocking means to refresh the favorites outside
   869  	// of the main loop.
   870  	FavoritesRefreshModeBlocking
   871  )
   872  
   873  // RefreshCache refreshes the cached list of favorites.
   874  //
   875  // In FavoritesRefreshModeBlocking, request the favorites in this function,
   876  // then send them to the main goroutine to be processed. This should be called
   877  // in a separate goroutine from anything mission-critical, because it might wait
   878  // on network for up to 15 seconds.
   879  //
   880  // In FavoritesRefreshModeInMainFavoritesLoop, this just sets up a request and
   881  // sends it to the main goroutine to process it - this is useful if e.g.
   882  // the favorites cache has not been initialized at all and cannot serve any
   883  // requests until this refresh is completed.
   884  func (f *Favorites) RefreshCache(ctx context.Context, mode FavoritesRefreshMode) {
   885  	f.muShutdown.RLock()
   886  	defer f.muShutdown.RUnlock()
   887  	if f.disabled || f.shutdown {
   888  		return
   889  	}
   890  
   891  	// Insert something into the refreshWaiting channel to guarantee that we
   892  	// are the only current refresh.
   893  	select {
   894  	case f.refreshWaiting <- struct{}{}:
   895  	default:
   896  		// There is already a refresh in the queue
   897  		return
   898  	}
   899  	// This request is non-blocking, so use a throw-away done channel
   900  	// and context. Note that in the `blocking` mode, this context will only
   901  	// be relevant for the brief moment the main loop processes the results
   902  	// generated in the below network request.
   903  	req := &favReq{
   904  		refresh: true,
   905  		done:    make(chan struct{}),
   906  		ctx:     context.Background(),
   907  	}
   908  	f.wg.Add(1)
   909  
   910  	if mode == FavoritesRefreshModeBlocking {
   911  		favResult, err := f.config.KBPKI().FavoriteList(ctx)
   912  		if err != nil {
   913  			f.log.CDebugf(ctx, "Failed to refresh cached Favorites: %+v", err)
   914  			// Because the request will not make it to the main processing
   915  			// loop, mark it as done and clear the refresh channel here.
   916  			f.wg.Done()
   917  			<-f.refreshWaiting
   918  			return
   919  		}
   920  		req.favResult = &favResult
   921  	}
   922  	select {
   923  	case f.reqChan <- req:
   924  		go func() {
   925  			<-req.done
   926  			if req.err != nil {
   927  				f.log.CDebugf(ctx, "Failed to refresh cached Favorites ("+
   928  					"error in main loop): %+v", req.err)
   929  			}
   930  		}()
   931  	case <-ctx.Done():
   932  		// Because the request will not make it to the main processing
   933  		// loop, mark it as done and clear the refresh channel here.
   934  		f.wg.Done()
   935  		<-f.refreshWaiting
   936  		return
   937  	}
   938  }
   939  
   940  // RefreshCacheWhenMTimeChanged refreshes the cached favorites, but
   941  // does so with rate-limiting, so that it doesn't hit the server too
   942  // often.  `id` is the ID of the TLF that caused this refresh to
   943  // happen.  As an optimization, if `id` matches the previous `id` that
   944  // triggered a refresh, then we just ignore the refresh since it won't
   945  // materially change the order of the favorites by mtime.
   946  func (f *Favorites) RefreshCacheWhenMTimeChanged(
   947  	ctx context.Context, id tlf.ID) {
   948  	f.muShutdown.RLock()
   949  	defer f.muShutdown.RUnlock()
   950  	if f.disabled || f.shutdown {
   951  		return
   952  	}
   953  
   954  	req := &favReq{
   955  		refresh:   true,
   956  		refreshID: id,
   957  		buffered:  true,
   958  		done:      make(chan struct{}),
   959  		ctx:       context.Background(),
   960  	}
   961  	f.bufferedWg.Add(1)
   962  	select {
   963  	case f.bufferedReqChan <- req:
   964  		go func() {
   965  			defer f.bufferedWg.Done()
   966  			select {
   967  			case <-req.done:
   968  				if req.err != nil {
   969  					f.log.CDebugf(ctx, "Failed to refresh cached Favorites ("+
   970  						"error in main loop): %+v", req.err)
   971  				}
   972  			case <-f.shutdownChan:
   973  			}
   974  		}()
   975  	default:
   976  		// There's already a buffered request waiting.
   977  		f.bufferedWg.Done()
   978  	}
   979  }
   980  
   981  // ClearCache clears the cached list of favorites.
   982  func (f *Favorites) ClearCache(ctx context.Context) {
   983  	f.muShutdown.RLock()
   984  	defer f.muShutdown.RUnlock()
   985  	if f.disabled || f.shutdown {
   986  		return
   987  	}
   988  	// This request is non-blocking, so use a throw-away done channel
   989  	// and context.
   990  	req := &favReq{
   991  		clear:   true,
   992  		refresh: false,
   993  		done:    make(chan struct{}),
   994  		ctx:     context.Background(),
   995  	}
   996  	f.wg.Add(1)
   997  	select {
   998  	case f.reqChan <- req:
   999  	case <-ctx.Done():
  1000  		f.wg.Done()
  1001  		return
  1002  	}
  1003  }
  1004  
  1005  // GetFolderWithFavFlags returns the a FolderWithFavFlags for give folder, if found.
  1006  func (f *Favorites) GetFolderWithFavFlags(
  1007  	ctx context.Context, fav favorites.Folder) (
  1008  	folderWithFavFlags *keybase1.FolderWithFavFlags, errr error) {
  1009  	if f.disabled {
  1010  		return nil, nil
  1011  	}
  1012  	f.muShutdown.RLock()
  1013  	defer f.muShutdown.RUnlock()
  1014  	if f.shutdown {
  1015  		return nil, data.ShutdownHappenedError{}
  1016  	}
  1017  	ch := make(chan *keybase1.FolderWithFavFlags, 1)
  1018  	req := &favReq{
  1019  		ctx:                ctx,
  1020  		toGet:              &fav,
  1021  		folderWithFavFlags: ch,
  1022  		done:               make(chan struct{}),
  1023  	}
  1024  	err := f.sendReq(ctx, req)
  1025  	if err != nil {
  1026  		return nil, err
  1027  	}
  1028  	folderWithFavFlags = <-ch
  1029  	return folderWithFavFlags, nil
  1030  }
  1031  
  1032  // Get returns the logged-in user's list of favorites. It uses the cache.
  1033  func (f *Favorites) Get(ctx context.Context) ([]favorites.Folder, error) {
  1034  	if f.disabled {
  1035  		session, err := f.config.KBPKI().GetCurrentSession(ctx)
  1036  		if err == nil {
  1037  			// Add favorites only for the current user.
  1038  			return []favorites.Folder{
  1039  				{Name: string(session.Name), Type: tlf.Private},
  1040  				{Name: string(session.Name), Type: tlf.Public},
  1041  			}, nil
  1042  		}
  1043  		return nil, nil
  1044  	}
  1045  	f.muShutdown.RLock()
  1046  	defer f.muShutdown.RUnlock()
  1047  	if f.shutdown {
  1048  		return nil, data.ShutdownHappenedError{}
  1049  	}
  1050  	favChan := make(chan []favorites.Folder, 1)
  1051  	req := &favReq{
  1052  		ctx:  ctx,
  1053  		favs: favChan,
  1054  		done: make(chan struct{}),
  1055  	}
  1056  	err := f.sendReq(ctx, req)
  1057  	if err != nil {
  1058  		return nil, err
  1059  	}
  1060  	return <-favChan, nil
  1061  }
  1062  
  1063  // setHomeTLFInfo should be called when a new user logs in so that their home
  1064  // TLFs can be returned as favorites.
  1065  func (f *Favorites) setHomeTLFInfo(ctx context.Context, info homeTLFInfo) {
  1066  	f.muShutdown.RLock()
  1067  	defer f.muShutdown.RUnlock()
  1068  	if f.shutdown {
  1069  		return
  1070  	}
  1071  	// This request is non-blocking, so use a throw-away done channel
  1072  	// and context.
  1073  	req := &favReq{
  1074  		homeTLFInfo: &info,
  1075  		done:        make(chan struct{}),
  1076  		ctx:         context.Background(),
  1077  	}
  1078  	f.wg.Add(1)
  1079  	select {
  1080  	case f.reqChan <- req:
  1081  	case <-ctx.Done():
  1082  		f.wg.Done()
  1083  		return
  1084  	}
  1085  }
  1086  
  1087  // GetAll returns the logged-in user's list of favorite, new, and ignored TLFs.
  1088  // It uses the cache.
  1089  func (f *Favorites) GetAll(ctx context.Context) (keybase1.FavoritesResult,
  1090  	error) {
  1091  	if f.disabled {
  1092  		session, err := f.config.KBPKI().GetCurrentSession(ctx)
  1093  		if err == nil {
  1094  			// Add favorites only for the current user.
  1095  			return keybase1.FavoritesResult{
  1096  				FavoriteFolders: []keybase1.Folder{
  1097  					{
  1098  						Name:       string(session.Name),
  1099  						Private:    false,
  1100  						Created:    false,
  1101  						FolderType: keybase1.FolderType_PUBLIC,
  1102  						TeamID:     &f.homeTLFInfo.PublicTeamID,
  1103  					},
  1104  					{
  1105  						Name:       string(session.Name),
  1106  						Private:    true,
  1107  						Created:    false,
  1108  						FolderType: keybase1.FolderType_PRIVATE,
  1109  						TeamID:     &f.homeTLFInfo.PrivateTeamID,
  1110  					},
  1111  				},
  1112  			}, nil
  1113  		}
  1114  		return keybase1.FavoritesResult{}, nil
  1115  	}
  1116  	f.muShutdown.RLock()
  1117  	defer f.muShutdown.RUnlock()
  1118  
  1119  	if f.shutdown {
  1120  		return keybase1.FavoritesResult{}, data.ShutdownHappenedError{}
  1121  	}
  1122  	favChan := make(chan keybase1.FavoritesResult, 1)
  1123  	req := &favReq{
  1124  		ctx:     ctx,
  1125  		favsAll: favChan,
  1126  		done:    make(chan struct{}),
  1127  	}
  1128  	err := f.sendReq(ctx, req)
  1129  	if err != nil {
  1130  		return keybase1.FavoritesResult{}, err
  1131  	}
  1132  	return <-favChan, nil
  1133  }