github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/subscription_manager.go (about) 1 // Copyright 2019 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 "path" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/keybase/client/go/kbfs/data" 14 "github.com/keybase/client/go/kbfs/tlfhandle" 15 "github.com/keybase/client/go/logger" 16 "github.com/keybase/client/go/protocol/keybase1" 17 "github.com/pkg/errors" 18 "golang.org/x/net/context" 19 "golang.org/x/time/rate" 20 ) 21 22 const ( 23 folderBranchPollingInterval = time.Second 24 maxPurgeableSubscriptionManagerClient = 3 25 ) 26 27 // userPath is always the full path including the /keybase prefix, but may 28 // not be canonical or cleaned. The goal is to track whatever the user of this 29 // type is dealing with without needing them to know if a path is canonicalized 30 // at any time. 31 // Examples: 32 // 33 // "/keybase/public/karlthefog@twitter/dir 34 // "/keybase/team/keybase/dir/../file" 35 type userPath string 36 37 // cleanInTlfPath is clean path rooted at a TLF, and it's what we get 38 // from Node.GetPathPlaintextSansTlf(). 39 // Examples, considering TLF /keybase/private/user1,user2: 40 // 41 // "/foo/bar" (representing /keybase/private/user1,user2/foo/bar) 42 // "/" (representing /keybase/private/user1,user2) 43 type cleanInTlfPath string 44 45 func getCleanInTlfPath(p *parsedPath) cleanInTlfPath { 46 return cleanInTlfPath(path.Clean(p.rawInTlfPath)) 47 } 48 49 func getParentPath(p cleanInTlfPath) (parent cleanInTlfPath, ok bool) { 50 lastSlashIndex := strings.LastIndex(string(p), "/") 51 if lastSlashIndex <= 0 { 52 return "", false 53 } 54 return p[:lastSlashIndex], true 55 } 56 57 type debouncedNotify struct { 58 notify func() 59 shutdown func() 60 } 61 62 func getChSender(ch chan<- struct{}, blocking bool) func() { 63 if blocking { 64 return func() { 65 ch <- struct{}{} 66 } 67 } 68 return func() { 69 select { 70 case ch <- struct{}{}: 71 default: 72 } 73 } 74 } 75 76 func debounce(do func(), limit rate.Limit) *debouncedNotify { 77 ctx, shutdown := context.WithCancel(context.Background()) 78 ch := make(chan struct{}, 1) 79 limiter := rate.NewLimiter(limit, 1) 80 go func() { 81 for { 82 err := limiter.Wait(ctx) 83 if err != nil { 84 return 85 } 86 select { 87 case <-ch: 88 go do() 89 case <-ctx.Done(): 90 return 91 } 92 } 93 }() 94 return &debouncedNotify{ 95 notify: getChSender(ch, limit == rate.Inf), 96 shutdown: shutdown, 97 } 98 } 99 100 type pathSubscriptionRef struct { 101 folderBranch data.FolderBranch 102 path cleanInTlfPath 103 } 104 105 type pathSubscription struct { 106 subscriptionIDs map[SubscriptionID]keybase1.PathSubscriptionTopic 107 // Keep track of different paths from input since GUI doesn't have a 108 // concept of "cleaned path" yet and when we notify about changes we need 109 // to use the original path that came in with the SubscribePath calls. 110 pathsToNotify map[string]struct{} 111 limit rate.Limit 112 debouncedNotify *debouncedNotify 113 } 114 115 type nonPathSubscription struct { 116 subscriptionIDs map[SubscriptionID]bool 117 limit rate.Limit 118 debouncedNotify *debouncedNotify 119 } 120 121 // subscriptionManager manages subscriptions. There are two types of 122 // subscriptions: path and non-path. Path subscriptions are for changes related 123 // to a specific path, such as file content change, dir children change, and 124 // timestamp change. Non-path subscriptions are for general changes that are 125 // not specific to a path, such as journal flushing, online status change, etc. 126 // We store a debouncedNotify struct for each subscription, which includes a 127 // notify function that might be debounced if caller asked so. 128 // 129 // This is per client. For example, if we have multiple GUI instances, each of 130 // them get their own client ID and their subscriptions won't affect each 131 // other. 132 type subscriptionManager struct { 133 clientID SubscriptionManagerClientID 134 config Config 135 log logger.Logger 136 notifier SubscriptionNotifier 137 138 onlineStatusTracker *onlineStatusTracker 139 lock sync.RWMutex 140 // TODO HOTPOT-416: add another layer here to reference by topics, and 141 // actually check topics in LocalChange and BatchChanges. 142 pathSubscriptions map[pathSubscriptionRef]*pathSubscription 143 pathSubscriptionIDToRef map[SubscriptionID]pathSubscriptionRef 144 nonPathSubscriptions map[keybase1.SubscriptionTopic]*nonPathSubscription 145 nonPathSubscriptionIDToTopic map[SubscriptionID]keybase1.SubscriptionTopic 146 subscriptionIDs map[SubscriptionID]bool 147 subscriptionCountByFolderBranch map[data.FolderBranch]int 148 folderBranchPollerCancelers map[SubscriptionID]context.CancelFunc 149 } 150 151 func (sm *subscriptionManager) notifyOnlineStatus() { 152 sm.lock.RLock() 153 defer sm.lock.RUnlock() 154 if sm.nonPathSubscriptions[keybase1.SubscriptionTopic_ONLINE_STATUS] == nil { 155 return 156 } 157 if nps, ok := sm.nonPathSubscriptions[keybase1.SubscriptionTopic_ONLINE_STATUS]; ok { 158 nps.debouncedNotify.notify() 159 } 160 } 161 162 func newSubscriptionManager(clientID SubscriptionManagerClientID, config Config, notifier SubscriptionNotifier) *subscriptionManager { 163 sm := &subscriptionManager{ 164 pathSubscriptions: make(map[pathSubscriptionRef]*pathSubscription), 165 pathSubscriptionIDToRef: make(map[SubscriptionID]pathSubscriptionRef), 166 nonPathSubscriptions: make(map[keybase1.SubscriptionTopic]*nonPathSubscription), 167 nonPathSubscriptionIDToTopic: make(map[SubscriptionID]keybase1.SubscriptionTopic), 168 clientID: clientID, 169 config: config, 170 log: config.MakeLogger("SubMan"), 171 notifier: notifier, 172 subscriptionIDs: make(map[SubscriptionID]bool), 173 subscriptionCountByFolderBranch: make(map[data.FolderBranch]int), 174 folderBranchPollerCancelers: make(map[SubscriptionID]context.CancelFunc), 175 } 176 sm.onlineStatusTracker = newOnlineStatusTracker(config, sm.notifyOnlineStatus) 177 return sm 178 } 179 180 func (sm *subscriptionManager) Shutdown(ctx context.Context) { 181 sm.onlineStatusTracker.shutdown() 182 sm.lock.Lock() 183 defer sm.lock.Unlock() 184 pathSids := make([]SubscriptionID, 0, len(sm.pathSubscriptionIDToRef)) 185 nonPathSids := make([]SubscriptionID, 0, len(sm.nonPathSubscriptionIDToTopic)) 186 for sid := range sm.pathSubscriptionIDToRef { 187 pathSids = append(pathSids, sid) 188 } 189 for sid := range sm.nonPathSubscriptionIDToTopic { 190 nonPathSids = append(nonPathSids, sid) 191 } 192 for _, sid := range pathSids { 193 sm.unsubscribePathLocked(ctx, sid) 194 } 195 for _, sid := range nonPathSids { 196 sm.unsubscribeNonPathLocked(ctx, sid) 197 } 198 } 199 200 func (sm *subscriptionManager) OnlineStatusTracker() OnlineStatusTracker { 201 return sm.onlineStatusTracker 202 } 203 204 func (sm *subscriptionManager) checkSubscriptionIDLocked(sid SubscriptionID) (setter func(), err error) { 205 if sm.subscriptionIDs[sid] { 206 return nil, errors.Errorf("duplicate subscription ID %q", sid) 207 } 208 return func() { 209 sm.subscriptionIDs[sid] = true 210 }, nil 211 } 212 213 func (sm *subscriptionManager) registerForChangesLocked(fb data.FolderBranch) { 214 if sm.subscriptionCountByFolderBranch[fb] == 0 { 215 _ = sm.config.Notifier().RegisterForChanges( 216 []data.FolderBranch{fb}, sm) 217 } 218 sm.subscriptionCountByFolderBranch[fb]++ 219 } 220 221 func (sm *subscriptionManager) unregisterForChangesLocked(fb data.FolderBranch) { 222 if sm.subscriptionCountByFolderBranch[fb] == 1 { 223 _ = sm.config.Notifier().UnregisterFromChanges( 224 []data.FolderBranch{fb}, sm) 225 delete(sm.subscriptionCountByFolderBranch, fb) 226 return 227 } 228 sm.subscriptionCountByFolderBranch[fb]-- 229 } 230 231 func (sm *subscriptionManager) preparePathNotification( 232 ref pathSubscriptionRef) (sids []SubscriptionID, 233 paths []string, topics []keybase1.PathSubscriptionTopic) { 234 sm.lock.RLock() 235 defer sm.lock.RUnlock() 236 237 ps, ok := sm.pathSubscriptions[ref] 238 if !ok { 239 return 240 } 241 sids = make([]SubscriptionID, 0, len(ps.subscriptionIDs)) 242 topicsMap := make(map[keybase1.PathSubscriptionTopic]struct{}) 243 for sid, topic := range ps.subscriptionIDs { 244 sids = append(sids, sid) 245 topicsMap[topic] = struct{}{} 246 } 247 topics = make([]keybase1.PathSubscriptionTopic, 0, len(topicsMap)) 248 for topic := range topicsMap { 249 topics = append(topics, topic) 250 } 251 paths = make([]string, 0, len(ps.pathsToNotify)) 252 for path := range ps.pathsToNotify { 253 paths = append(paths, path) 254 } 255 return sids, paths, topics 256 } 257 258 func (sm *subscriptionManager) makePathSubscriptionDebouncedNotify( 259 ref pathSubscriptionRef, limit rate.Limit) *debouncedNotify { 260 return debounce(func() { 261 sids, paths, topics := sm.preparePathNotification(ref) 262 263 for _, path := range paths { 264 sm.notifier.OnPathChange(sm.clientID, sids, path, topics) 265 } 266 }, limit) 267 } 268 269 func (sm *subscriptionManager) prepareNonPathNotification( 270 topic keybase1.SubscriptionTopic) (sids []SubscriptionID) { 271 sm.lock.RLock() 272 defer sm.lock.RUnlock() 273 nps, ok := sm.nonPathSubscriptions[topic] 274 if !ok { 275 return 276 } 277 sids = make([]SubscriptionID, 0, len(nps.subscriptionIDs)) 278 for sid := range nps.subscriptionIDs { 279 sids = append(sids, sid) 280 } 281 return sids 282 } 283 284 func (sm *subscriptionManager) makeNonPathSubscriptionDebouncedNotify( 285 topic keybase1.SubscriptionTopic, limit rate.Limit) *debouncedNotify { 286 return debounce(func() { 287 sids := sm.prepareNonPathNotification(topic) 288 sm.notifier.OnNonPathChange(sm.clientID, sids, topic) 289 }, limit) 290 } 291 292 type subscribePathRequest struct { 293 sid SubscriptionID 294 path string // original, uncleaned path from GUI 295 topic keybase1.PathSubscriptionTopic 296 deduplicateInterval *time.Duration 297 } 298 299 func (sm *subscriptionManager) subscribePathWithFolderBranchLocked( 300 req subscribePathRequest, 301 parsedPath *parsedPath, fb data.FolderBranch) error { 302 nitp := getCleanInTlfPath(parsedPath) 303 ref := pathSubscriptionRef{ 304 folderBranch: fb, 305 path: nitp, 306 } 307 308 subscriptionIDSetter, err := sm.checkSubscriptionIDLocked(req.sid) 309 if err != nil { 310 return err 311 } 312 sm.registerForChangesLocked(ref.folderBranch) 313 314 limit := rate.Inf 315 if req.deduplicateInterval != nil { 316 limit = rate.Every(*req.deduplicateInterval) 317 } 318 ps, ok := sm.pathSubscriptions[ref] 319 if !ok { 320 ps = &pathSubscription{ 321 subscriptionIDs: make(map[SubscriptionID]keybase1.PathSubscriptionTopic), 322 limit: limit, 323 debouncedNotify: sm.makePathSubscriptionDebouncedNotify(ref, limit), 324 pathsToNotify: make(map[string]struct{}), 325 } 326 sm.pathSubscriptions[ref] = ps 327 } else if ps.limit < limit { 328 // New limit is higher than what we have. Update it to match. 329 ps.limit = limit 330 ps.debouncedNotify.shutdown() 331 ps.debouncedNotify = sm.makePathSubscriptionDebouncedNotify(ref, limit) 332 } 333 ps.subscriptionIDs[req.sid] = req.topic 334 ps.pathsToNotify[req.path] = struct{}{} 335 336 sm.pathSubscriptionIDToRef[req.sid] = ref 337 subscriptionIDSetter() 338 return nil 339 } 340 341 func (sm *subscriptionManager) cancelAndDeleteFolderBranchPollerLocked( 342 sid SubscriptionID) (deleted bool) { 343 if cancel, ok := sm.folderBranchPollerCancelers[sid]; ok { 344 cancel() 345 delete(sm.folderBranchPollerCancelers, sid) 346 return true 347 } 348 return false 349 } 350 351 func (sm *subscriptionManager) cancelAndDeleteFolderBranchPoller( 352 sid SubscriptionID) (deleted bool) { 353 sm.lock.Lock() 354 defer sm.lock.Unlock() 355 return sm.cancelAndDeleteFolderBranchPollerLocked(sid) 356 } 357 358 func (sm *subscriptionManager) pollOnFolderBranchForSubscribePathRequest( 359 ctx context.Context, req subscribePathRequest, parsedPath *parsedPath) { 360 ticker := time.NewTicker(folderBranchPollingInterval) 361 for { 362 select { 363 case <-ticker.C: 364 fb, err := parsedPath.getFolderBranch(ctx, sm.config) 365 if err != nil { 366 _ = sm.cancelAndDeleteFolderBranchPoller(req.sid) 367 return 368 } 369 370 if fb == (data.FolderBranch{}) { 371 continue 372 } 373 374 // We have a folderBranch now! Go ahead and complete the 375 // subscription, and send a notification too. 376 377 sm.lock.Lock() 378 defer sm.lock.Unlock() 379 // Check if we're done while holding the lock to protect 380 // against racing against unsubscribe. 381 select { 382 case <-ctx.Done(): 383 // No need to call cancelAndDeleteFolderBranchPollerLocked here 384 // since we always cancel and delete at the same tiem and if 385 // it's canceled it must have been deleted too. 386 return 387 default: 388 } 389 390 err = sm.subscribePathWithFolderBranchLocked(req, parsedPath, fb) 391 if err != nil { 392 sm.log.CErrorf(ctx, 393 "subscribePathWithFolderBranchLocked sid=%s err=%v", req.sid, err) 394 } 395 396 sm.notifier.OnPathChange( 397 sm.clientID, []SubscriptionID{req.sid}, 398 req.path, []keybase1.PathSubscriptionTopic{req.topic}) 399 400 _ = sm.cancelAndDeleteFolderBranchPollerLocked(req.sid) 401 return 402 case <-ctx.Done(): 403 _ = sm.cancelAndDeleteFolderBranchPoller(req.sid) 404 return 405 } 406 } 407 } 408 409 func (sm *subscriptionManager) subscribePathWithoutFolderBranchLocked( 410 req subscribePathRequest, parsedPath *parsedPath) { 411 ctx, cancel := context.WithCancel(context.Background()) 412 sm.folderBranchPollerCancelers[req.sid] = cancel 413 go sm.pollOnFolderBranchForSubscribePathRequest(ctx, req, parsedPath) 414 } 415 416 // SubscribePath implements the SubscriptionManager interface. 417 func (sm *subscriptionManager) SubscribePath(ctx context.Context, 418 sid SubscriptionID, path string, topic keybase1.PathSubscriptionTopic, 419 deduplicateInterval *time.Duration) error { 420 parsedPath, err := parsePath(userPath(path)) 421 if err != nil { 422 return err 423 } 424 425 // Lock here to protect against racing with unsubscribe. Specifically, we 426 // don't want to launch the poller if an unsubscribe call for this sid 427 // comes in before we get fb from parsedPath.getFolderBranch(). 428 // 429 // We could still end up with a lingering subscription if unsubscribe 430 // happens too fast and RPC somehow gives use the unsubscribe call before 431 // the subscribe call, but that's probably rare enough to ignore here. 432 // 433 // In the future if this end up contributing a deadlock because 434 // folderBranch starts using the subscription manager somehow, we can add a 435 // "recently unsubscribed" cache to the subscription manager and move this 436 // lock further down. This cache should also mitigate the issue where the 437 // unsubscribe call gets deliverd before subscribe. 438 sm.lock.Lock() 439 defer sm.lock.Unlock() 440 441 fb, err := parsedPath.getFolderBranch(ctx, sm.config) 442 if err != nil { 443 return err 444 } 445 req := subscribePathRequest{ 446 sid: sid, 447 path: path, 448 topic: topic, 449 deduplicateInterval: deduplicateInterval, 450 } 451 if fb != (data.FolderBranch{}) { 452 return sm.subscribePathWithFolderBranchLocked(req, parsedPath, fb) 453 } 454 sm.subscribePathWithoutFolderBranchLocked(req, parsedPath) 455 return nil 456 } 457 458 // SubscribeNonPath implements the SubscriptionManager interface. 459 func (sm *subscriptionManager) SubscribeNonPath( 460 ctx context.Context, sid SubscriptionID, topic keybase1.SubscriptionTopic, 461 deduplicateInterval *time.Duration) error { 462 sm.lock.Lock() 463 defer sm.lock.Unlock() 464 subscriptionIDSetter, err := sm.checkSubscriptionIDLocked(sid) 465 if err != nil { 466 return err 467 } 468 469 limit := rate.Inf 470 if deduplicateInterval != nil { 471 limit = rate.Every(*deduplicateInterval) 472 } 473 nps, ok := sm.nonPathSubscriptions[topic] 474 if !ok { 475 nps = &nonPathSubscription{ 476 subscriptionIDs: make(map[SubscriptionID]bool), 477 limit: limit, 478 debouncedNotify: sm.makeNonPathSubscriptionDebouncedNotify(topic, limit), 479 } 480 sm.nonPathSubscriptions[topic] = nps 481 } else if nps.limit < limit { 482 // New limit is higher than what we have. Update it to match. 483 nps.limit = limit 484 nps.debouncedNotify.shutdown() 485 nps.debouncedNotify = sm.makeNonPathSubscriptionDebouncedNotify(topic, limit) 486 } 487 nps.subscriptionIDs[sid] = true 488 489 sm.nonPathSubscriptionIDToTopic[sid] = topic 490 subscriptionIDSetter() 491 return nil 492 } 493 494 func (sm *subscriptionManager) unsubscribePathLocked( 495 ctx context.Context, subscriptionID SubscriptionID) { 496 // First check if this is a subscription we don't yet have a folderBranch 497 // for. 498 if sm.cancelAndDeleteFolderBranchPollerLocked(subscriptionID) { 499 return 500 } 501 502 ref, ok := sm.pathSubscriptionIDToRef[subscriptionID] 503 if !ok { 504 return 505 } 506 delete(sm.pathSubscriptionIDToRef, subscriptionID) 507 508 ps, ok := sm.pathSubscriptions[ref] 509 if !ok { 510 return 511 } 512 delete(ps.subscriptionIDs, subscriptionID) 513 if len(ps.subscriptionIDs) == 0 { 514 ps.debouncedNotify.shutdown() 515 sm.unregisterForChangesLocked(ref.folderBranch) 516 delete(sm.pathSubscriptions, ref) 517 } 518 519 delete(sm.subscriptionIDs, subscriptionID) 520 } 521 522 func (sm *subscriptionManager) unsubscribeNonPathLocked( 523 ctx context.Context, subscriptionID SubscriptionID) { 524 topic, ok := sm.nonPathSubscriptionIDToTopic[subscriptionID] 525 if !ok { 526 return 527 } 528 delete(sm.nonPathSubscriptionIDToTopic, subscriptionID) 529 530 nps, ok := sm.nonPathSubscriptions[topic] 531 if !ok { 532 return 533 } 534 delete(nps.subscriptionIDs, subscriptionID) 535 if len(nps.subscriptionIDs) == 0 { 536 nps.debouncedNotify.shutdown() 537 delete(sm.nonPathSubscriptions, topic) 538 } 539 540 delete(sm.subscriptionIDs, subscriptionID) 541 } 542 543 // Unsubscribe implements the SubscriptionManager interface. 544 func (sm *subscriptionManager) Unsubscribe(ctx context.Context, sid SubscriptionID) { 545 sm.lock.Lock() 546 defer sm.lock.Unlock() 547 sm.unsubscribePathLocked(ctx, sid) 548 sm.unsubscribeNonPathLocked(ctx, sid) 549 } 550 551 func (sm *subscriptionManager) notifyRefLocked(ref pathSubscriptionRef) { 552 ps, ok := sm.pathSubscriptions[ref] 553 if !ok { 554 return 555 } 556 // We are notify()-ing while holding a lock, but it's fine since the 557 // other side of the channel consumes it pretty fast, either by 558 // dropping deduplicated ones, or by doing the actual send in a 559 // separate goroutine. 560 // 561 // We are not differentiating topics here yet. TODO: do it. 562 ps.debouncedNotify.notify() 563 } 564 565 func (sm *subscriptionManager) nodeChangeLocked(node Node) { 566 path, ok := node.GetPathPlaintextSansTlf() 567 if !ok { 568 return 569 } 570 cleanPath := cleanInTlfPath(path) 571 572 sm.notifyRefLocked(pathSubscriptionRef{ 573 folderBranch: node.GetFolderBranch(), 574 path: cleanPath, 575 }) 576 577 // Do this for parent as well, so if "children" is subscribed on parent 578 // path, we'd trigger a notification too. 579 if parent, ok := getParentPath(cleanPath); ok { 580 sm.notifyRefLocked(pathSubscriptionRef{ 581 folderBranch: node.GetFolderBranch(), 582 path: parent, 583 }) 584 } 585 } 586 587 var _ SubscriptionManagerPublisher = (*subscriptionManager)(nil) 588 589 // PublishChange implements the SubscriptionManagerPublisher interface. 590 func (sm *subscriptionManager) PublishChange(topic keybase1.SubscriptionTopic) { 591 sm.lock.RLock() 592 defer sm.lock.RUnlock() 593 594 // When sync status changes, trigger notification for all paths so they 595 // reload to get new prefetch status. This is unfortunate but it's 596 // non-trivial to actually build notification around individuall path's 597 // prefetch status. Since GUI doesnt' have that many path notifications, 598 // this should be fine. 599 // 600 // TODO: Build it. 601 if topic == keybase1.SubscriptionTopic_OVERALL_SYNC_STATUS { 602 for _, ps := range sm.pathSubscriptions { 603 ps.debouncedNotify.notify() 604 } 605 } 606 607 if nps, ok := sm.nonPathSubscriptions[topic]; ok { 608 nps.debouncedNotify.notify() 609 } 610 } 611 612 var _ Observer = (*subscriptionManager)(nil) 613 614 // LocalChange implements the Observer interface. 615 func (sm *subscriptionManager) LocalChange(ctx context.Context, 616 node Node, write WriteRange) { 617 sm.lock.RLock() 618 defer sm.lock.RUnlock() 619 // TODO HOTPOT-416: check topics 620 sm.nodeChangeLocked(node) 621 } 622 623 // BatchChanges implements the Observer interface. 624 func (sm *subscriptionManager) BatchChanges(ctx context.Context, 625 changes []NodeChange, allAffectedNodeIDs []NodeID) { 626 sm.lock.RLock() 627 defer sm.lock.RUnlock() 628 // TODO HOTPOT-416: check topics 629 for _, change := range changes { 630 sm.nodeChangeLocked(change.Node) 631 } 632 } 633 634 // TlfHandleChange implements the Observer interface. 635 func (sm *subscriptionManager) TlfHandleChange(ctx context.Context, 636 newHandle *tlfhandle.Handle) { 637 } 638 639 type subscriptionManagerManager struct { 640 lock sync.RWMutex 641 config Config 642 subscriptionManagers map[SubscriptionManagerClientID]*subscriptionManager 643 purgeableClientIDsFIFO []SubscriptionManagerClientID 644 } 645 646 func newSubscriptionManagerManager(config Config) *subscriptionManagerManager { 647 return &subscriptionManagerManager{ 648 config: config, 649 subscriptionManagers: make(map[SubscriptionManagerClientID]*subscriptionManager), 650 purgeableClientIDsFIFO: nil, 651 } 652 } 653 654 func (smm *subscriptionManagerManager) Shutdown(ctx context.Context) { 655 smm.lock.Lock() 656 defer smm.lock.Unlock() 657 658 for _, sm := range smm.subscriptionManagers { 659 sm.Shutdown(ctx) 660 } 661 smm.subscriptionManagers = make(map[SubscriptionManagerClientID]*subscriptionManager) 662 smm.purgeableClientIDsFIFO = nil 663 } 664 665 func (smm *subscriptionManagerManager) get( 666 clientID SubscriptionManagerClientID, purgeable bool, 667 notifier SubscriptionNotifier) *subscriptionManager { 668 smm.lock.RLock() 669 sm, ok := smm.subscriptionManagers[clientID] 670 smm.lock.RUnlock() 671 672 if ok { 673 return sm 674 } 675 676 smm.lock.Lock() 677 defer smm.lock.Unlock() 678 679 // Check again under the lock in case we've already created one. This is 680 // important since if we create it twice we'd end up with having the same 681 // clientID appearing twice in purgeableClientIDsFIFO and when we purge the 682 // second one we'd have a panic. 683 sm, ok = smm.subscriptionManagers[clientID] 684 if ok { 685 return sm 686 } 687 688 if purgeable { 689 if len(smm.purgeableClientIDsFIFO) == maxPurgeableSubscriptionManagerClient { 690 toPurge := smm.purgeableClientIDsFIFO[0] 691 smm.subscriptionManagers[toPurge].Shutdown(context.Background()) 692 delete(smm.subscriptionManagers, toPurge) 693 smm.purgeableClientIDsFIFO = smm.purgeableClientIDsFIFO[1:] 694 } 695 smm.purgeableClientIDsFIFO = append(smm.purgeableClientIDsFIFO, clientID) 696 } 697 698 sm = newSubscriptionManager(clientID, smm.config, notifier) 699 smm.subscriptionManagers[clientID] = sm 700 701 return sm 702 } 703 704 // PublishChange implements the SubscriptionManagerPublisher interface. 705 func (smm *subscriptionManagerManager) PublishChange(topic keybase1.SubscriptionTopic) { 706 smm.lock.RLock() 707 defer smm.lock.RUnlock() 708 for _, sm := range smm.subscriptionManagers { 709 sm.PublishChange(topic) 710 } 711 }