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 }