github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/sync.go (about) 1 package chat 2 3 import ( 4 "encoding/hex" 5 "sort" 6 "sync" 7 "time" 8 9 "github.com/keybase/client/go/protocol/keybase1" 10 11 "github.com/keybase/client/go/chat/globals" 12 "github.com/keybase/client/go/chat/storage" 13 "github.com/keybase/client/go/chat/types" 14 "github.com/keybase/client/go/chat/utils" 15 "github.com/keybase/client/go/protocol/chat1" 16 "github.com/keybase/client/go/protocol/gregor1" 17 "github.com/keybase/clockwork" 18 "golang.org/x/net/context" 19 ) 20 21 type Syncer struct { 22 globals.Contextified 23 utils.DebugLabeler 24 sync.Mutex 25 26 isConnected bool 27 offlinables []types.Offlinable 28 29 notificationLock sync.Mutex 30 lastLoadedLock sync.Mutex 31 clock clockwork.Clock 32 sendDelay time.Duration 33 shutdownCh chan struct{} 34 fullReloadCh chan gregor1.UID 35 flushCh chan struct{} 36 notificationQueue map[string][]chat1.ConversationStaleUpdate 37 fullReload map[string]bool 38 lastLoadedConv chat1.ConversationID 39 maxLimitedConvLoads int 40 maxConvLoads int 41 } 42 43 func NewSyncer(g *globals.Context) *Syncer { 44 s := &Syncer{ 45 Contextified: globals.NewContextified(g), 46 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Syncer", false), 47 isConnected: false, 48 clock: clockwork.NewRealClock(), 49 shutdownCh: make(chan struct{}), 50 fullReloadCh: make(chan gregor1.UID), 51 flushCh: make(chan struct{}), 52 notificationQueue: make(map[string][]chat1.ConversationStaleUpdate), 53 fullReload: make(map[string]bool), 54 sendDelay: time.Millisecond * 1000, 55 maxLimitedConvLoads: 3, 56 maxConvLoads: 10, 57 } 58 go s.sendNotificationLoop() 59 return s 60 } 61 62 func (s *Syncer) SetClock(clock clockwork.Clock) { 63 s.clock = clock 64 } 65 66 func (s *Syncer) Shutdown() { 67 s.Debug(context.Background(), "shutting down") 68 close(s.shutdownCh) 69 } 70 71 func (s *Syncer) dedupUpdates(updates []chat1.ConversationStaleUpdate) (res []chat1.ConversationStaleUpdate) { 72 m := make(map[chat1.ConvIDStr]chat1.ConversationStaleUpdate) 73 for _, update := range updates { 74 if existing, ok := m[update.ConvID.ConvIDStr()]; ok { 75 switch existing.UpdateType { 76 case chat1.StaleUpdateType_CLEAR: 77 // do nothing, existing is already clearing 78 case chat1.StaleUpdateType_NEWACTIVITY: 79 m[update.ConvID.ConvIDStr()] = update 80 } 81 } else { 82 m[update.ConvID.ConvIDStr()] = update 83 } 84 } 85 for _, update := range m { 86 res = append(res, update) 87 } 88 return res 89 } 90 91 func (s *Syncer) sendNotificationsOnce() { 92 s.notificationLock.Lock() 93 defer s.notificationLock.Unlock() 94 95 // Broadcast full reloads 96 for uid := range s.fullReload { 97 s.Debug(context.Background(), "flushing full reload: uid: %s", uid) 98 b, _ := hex.DecodeString(uid) 99 s.G().ActivityNotifier.InboxStale(context.Background(), gregor1.UID(b)) 100 } 101 s.fullReload = make(map[string]bool) 102 // Broadcast conversation stales 103 for uid, updates := range s.notificationQueue { 104 updates = s.dedupUpdates(updates) 105 b, _ := hex.DecodeString(uid) 106 s.Debug(context.Background(), "flushing notifications: uid: %s len: %d", uid, len(updates)) 107 for _, update := range updates { 108 s.Debug(context.Background(), "flushing: uid: %s convID: %s type: %v", uid, 109 update.ConvID, update.UpdateType) 110 } 111 s.G().ActivityNotifier.ThreadsStale(context.Background(), gregor1.UID(b), updates) 112 } 113 s.notificationQueue = make(map[string][]chat1.ConversationStaleUpdate) 114 } 115 116 func (s *Syncer) sendNotificationLoop() { 117 s.Debug(context.Background(), "starting notification loop") 118 for { 119 select { 120 case <-s.shutdownCh: 121 return 122 case uid := <-s.fullReloadCh: 123 s.notificationLock.Lock() 124 s.fullReload[uid.String()] = true 125 delete(s.notificationQueue, uid.String()) 126 s.notificationLock.Unlock() 127 s.sendNotificationsOnce() 128 case <-s.clock.After(s.sendDelay): 129 s.sendNotificationsOnce() 130 case <-s.flushCh: 131 s.sendNotificationsOnce() 132 } 133 } 134 } 135 136 func (s *Syncer) SendChatStaleNotifications(ctx context.Context, uid gregor1.UID, 137 updates []chat1.ConversationStaleUpdate, immediate bool) { 138 if len(updates) == 0 { 139 s.Debug(ctx, "sending inbox stale message") 140 s.fullReloadCh <- uid 141 } else { 142 s.Debug(ctx, "sending thread stale messages: len: %d", len(updates)) 143 for _, update := range updates { 144 s.Debug(ctx, "sending thread stale message: convID: %s type: %v", update.ConvID, 145 update.UpdateType) 146 } 147 s.notificationLock.Lock() 148 if !s.fullReload[uid.String()] { 149 s.notificationQueue[uid.String()] = append(s.notificationQueue[uid.String()], updates...) 150 } 151 s.notificationLock.Unlock() 152 if immediate { 153 s.flushCh <- struct{}{} 154 } 155 } 156 } 157 158 func (s *Syncer) isServerInboxClear(ctx context.Context, inbox *storage.Inbox, srvVers int) bool { 159 if _, err := s.G().ServerCacheVersions.MatchInbox(ctx, srvVers); err != nil { 160 s.Debug(ctx, "isServerInboxClear: inbox server version match error: %s", err.Error()) 161 return true 162 } 163 164 return false 165 } 166 167 func (s *Syncer) IsConnected(ctx context.Context) bool { 168 s.Lock() 169 defer s.Unlock() 170 return s.isConnected 171 } 172 173 func (s *Syncer) Connected(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID, 174 syncRes *chat1.SyncChatRes) (err error) { 175 ctx = globals.CtxAddLogTags(ctx, s.G()) 176 defer s.Trace(ctx, &err, "Connected")() 177 s.Lock() 178 s.isConnected = true 179 // Let the Offlinables know that we are back online 180 for _, o := range s.offlinables { 181 o.Connected(ctx) 182 } 183 s.Unlock() 184 185 // Run sync against the server 186 return s.Sync(ctx, cli, uid, syncRes) 187 } 188 189 func (s *Syncer) Disconnected(ctx context.Context) { 190 defer s.Trace(ctx, nil, "Disconnected")() 191 s.Lock() 192 s.isConnected = false 193 // Let the Offlinables know of connection state change 194 for _, o := range s.offlinables { 195 o.Disconnected(ctx) 196 } 197 s.Unlock() 198 } 199 200 func (s *Syncer) handleMembersTypeChanged(ctx context.Context, uid gregor1.UID, 201 convIDs []chat1.ConversationID) { 202 // Clear caches from members type changed convos 203 for _, convID := range convIDs { 204 s.Debug(ctx, "handleMembersTypeChanged: clearing message cache: %s", convID) 205 err := s.G().ConvSource.Clear(ctx, convID, uid, nil) 206 if err != nil { 207 s.Debug(ctx, "handleMembersTypeChanged: erroring clearing conv: %+v", err) 208 } 209 } 210 } 211 212 func (s *Syncer) handleFilteredConvs(ctx context.Context, uid gregor1.UID, syncConvs []chat1.Conversation, 213 filteredConvs []types.RemoteConversation) { 214 fmap := make(map[chat1.ConvIDStr]bool) 215 for _, fconv := range filteredConvs { 216 fmap[fconv.Conv.GetConvID().ConvIDStr()] = true 217 } 218 // If any sync convs are not in the filtered list, let's blow away their local storage 219 for _, sconv := range syncConvs { 220 if !fmap[sconv.GetConvID().ConvIDStr()] { 221 s.Debug(ctx, "handleFilteredConvs: conv filtered from inbox, removing cache: convID: %s memberStatus: %v existence: %v", 222 sconv.GetConvID(), sconv.ReaderInfo.Status, sconv.Metadata.Existence) 223 err := s.G().ConvSource.Clear(ctx, sconv.GetConvID(), uid, nil) 224 if err != nil { 225 s.Debug(ctx, "handleFilteredCovs: erroring clearing conv: %+v", err) 226 } 227 } 228 } 229 } 230 231 func (s *Syncer) maxSyncUnboxConvs() int { 232 if s.G().IsMobileAppType() { 233 return 8 234 } 235 return 100 236 } 237 238 func (s *Syncer) getShouldUnboxSyncConvMap(ctx context.Context, convs []chat1.Conversation, 239 topicNameChanged []chat1.ConversationID) (m map[chat1.ConvIDStr]bool) { 240 m = make(map[chat1.ConvIDStr]bool) 241 for _, t := range topicNameChanged { 242 m[t.ConvIDStr()] = true 243 } 244 rconvs := utils.RemoteConvs(convs) 245 sort.Slice(rconvs, func(i, j int) bool { 246 return utils.GetConvPriorityScore(rconvs[i]) >= utils.GetConvPriorityScore(rconvs[j]) 247 }) 248 maxConvs := s.maxSyncUnboxConvs() 249 for _, conv := range rconvs { 250 if len(m) >= maxConvs { 251 s.Debug(ctx, "getShouldUnboxSyncConvMap: max sync convs reached, not including any others") 252 break 253 } 254 if m[conv.ConvIDStr] { 255 continue 256 } 257 if s.shouldUnboxSyncConv(conv.Conv) { 258 m[conv.ConvIDStr] = true 259 } 260 } 261 return m 262 } 263 264 func (s *Syncer) shouldUnboxSyncConv(conv chat1.Conversation) bool { 265 // only chat on mobile 266 if s.G().IsMobileAppType() && conv.GetTopicType() != chat1.TopicType_CHAT { 267 return false 268 } 269 // Skips convs we don't care for. 270 switch conv.Metadata.Status { 271 case chat1.ConversationStatus_BLOCKED, 272 chat1.ConversationStatus_IGNORED, 273 chat1.ConversationStatus_REPORTED: 274 return false 275 } 276 // Only let through ACTIVE/PREVIEW convs. 277 if conv.ReaderInfo != nil { 278 switch conv.ReaderInfo.Status { 279 case chat1.ConversationMemberStatus_ACTIVE, 280 chat1.ConversationMemberStatus_PREVIEW: 281 default: 282 return false 283 } 284 } 285 switch conv.GetMembersType() { 286 case chat1.ConversationMembersType_TEAM: 287 // include if this is a simple team or we are currently viewing the 288 // conv. 289 return conv.GetTopicType() == chat1.TopicType_KBFSFILEEDIT || 290 conv.Metadata.TeamType != chat1.TeamType_COMPLEX || 291 conv.GetConvID().Eq(s.GetSelectedConversation()) 292 default: 293 return true 294 } 295 } 296 297 func (s *Syncer) notifyIncrementalSync(ctx context.Context, uid gregor1.UID, 298 allConvs []chat1.Conversation, shouldUnboxMap map[chat1.ConvIDStr]bool) { 299 if len(allConvs) == 0 { 300 s.Debug(ctx, "notifyIncrementalSync: no conversations given, sending a current result") 301 s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, 302 chat1.NewChatSyncResultWithCurrent()) 303 return 304 } 305 itemsByTopicType := make(map[chat1.TopicType][]chat1.ChatSyncIncrementalConv) 306 for _, c := range allConvs { 307 var md *types.RemoteConversationMetadata 308 rc, err := utils.GetUnverifiedConv(ctx, s.G(), uid, c.GetConvID(), 309 types.InboxSourceDataSourceLocalOnly) 310 if err == nil { 311 md = rc.LocalMetadata 312 } 313 rc = utils.RemoteConv(c) 314 rc.LocalMetadata = md 315 itemsByTopicType[c.GetTopicType()] = append(itemsByTopicType[c.GetTopicType()], 316 chat1.ChatSyncIncrementalConv{ 317 Conv: utils.PresentRemoteConversation(ctx, s.G(), uid, rc), 318 ShouldUnbox: shouldUnboxMap[c.GetConvID().ConvIDStr()], 319 }) 320 } 321 for _, topicType := range chat1.TopicTypeMap { 322 if topicType == chat1.TopicType_NONE { 323 continue 324 } 325 s.G().ActivityNotifier.InboxSynced(ctx, uid, topicType, 326 chat1.NewChatSyncResultWithIncremental(chat1.ChatSyncIncrementalInfo{ 327 Items: itemsByTopicType[topicType], 328 })) 329 } 330 } 331 332 func (s *Syncer) Sync(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID, 333 syncRes *chat1.SyncChatRes) (err error) { 334 defer s.Trace(ctx, &err, "Sync")() 335 s.Lock() 336 if !s.isConnected { 337 defer s.Unlock() 338 s.Debug(ctx, "Sync: aborting because currently offline") 339 return OfflineError{} 340 } 341 s.Unlock() 342 343 // Grab current on disk version 344 ibox := storage.NewInbox(s.G()) 345 vers, err := ibox.Version(ctx, uid) 346 if err != nil { 347 return err 348 } 349 srvVers, err := ibox.ServerVersion(ctx, uid) 350 if err != nil { 351 return err 352 } 353 s.Debug(ctx, "Sync: current inbox version: %v server version: %d", vers, srvVers) 354 355 if syncRes == nil { 356 // Run the sync call on the server to see how current our local copy is 357 syncRes = new(chat1.SyncChatRes) 358 if *syncRes, err = cli.SyncChat(ctx, chat1.SyncChatArg{ 359 Vers: vers, 360 SummarizeMaxMsgs: true, 361 ParticipantsMode: chat1.InboxParticipantsMode_SKIP_TEAMS, 362 }); err != nil { 363 s.Debug(ctx, "Sync: failed to sync inbox: %s", err.Error()) 364 return err 365 } 366 } else { 367 s.Debug(ctx, "Sync: skipping sync call, data provided") 368 } 369 370 // Set new server versions 371 if err = s.G().ServerCacheVersions.Set(ctx, syncRes.CacheVers); err != nil { 372 s.Debug(ctx, "Sync: failed to set new server versions: %s", err.Error()) 373 } 374 375 // Process what the server has told us to do with the local inbox copy 376 rtyp, err := syncRes.InboxRes.Typ() 377 if err != nil { 378 s.Debug(ctx, "Sync: strange type from SyncInbox: %s", err.Error()) 379 return err 380 } 381 // Check if the server has cleared the inbox 382 if s.isServerInboxClear(ctx, ibox, srvVers) { 383 rtyp = chat1.SyncInboxResType_CLEAR 384 } 385 386 switch rtyp { 387 case chat1.SyncInboxResType_CLEAR: 388 s.Debug(ctx, "Sync: version out of date, clearing inbox: %v", vers) 389 if err = ibox.Clear(ctx, uid); err != nil { 390 s.Debug(ctx, "Sync: failed to clear inbox: %s", err.Error()) 391 } 392 // Send notifications for a full clear 393 s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, 394 chat1.NewChatSyncResultWithClear()) 395 case chat1.SyncInboxResType_CURRENT: 396 s.Debug(ctx, "Sync: version is current, standing pat: %v", vers) 397 s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, 398 chat1.NewChatSyncResultWithCurrent()) 399 case chat1.SyncInboxResType_INCREMENTAL: 400 incr := syncRes.InboxRes.Incremental() 401 s.Debug(ctx, "Sync: version out of date, but can incrementally sync: old vers: %v vers: %v convs: %d", 402 vers, incr.Vers, len(incr.Convs)) 403 404 var iboxSyncRes types.InboxSyncRes 405 expunges := make(map[chat1.ConvIDStr]chat1.Expunge) 406 if iboxSyncRes, err = s.G().InboxSource.Sync(ctx, uid, incr.Vers, incr.Convs); err != nil { 407 s.Debug(ctx, "Sync: failed to sync conversations to inbox: %s", err.Error()) 408 409 // Send notifications for a full clear 410 s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, 411 chat1.NewChatSyncResultWithClear()) 412 } else { 413 s.handleMembersTypeChanged(ctx, uid, iboxSyncRes.MembersTypeChanged) 414 s.handleFilteredConvs(ctx, uid, incr.Convs, iboxSyncRes.FilteredConvs) 415 for _, expunge := range iboxSyncRes.Expunges { 416 expunges[expunge.ConvID.ConvIDStr()] = expunge.Expunge 417 } 418 // Send notifications for a successful partial sync 419 shouldUnboxMap := s.getShouldUnboxSyncConvMap(ctx, incr.Convs, iboxSyncRes.TopicNameChanged) 420 s.notifyIncrementalSync(ctx, uid, incr.Convs, shouldUnboxMap) 421 } 422 423 // The idea here is to limit the amount of work we do with the 424 // background conversation loader on mobile. If we are on a cell 425 // connection, or if we just came into the foreground, limit the number 426 // of conversations we queue up for background loading. 427 var queuedConvs, maxConvs int 428 pageBack := 3 429 num := 50 430 netState := s.G().MobileNetState.State() 431 state := s.G().MobileAppState.State() 432 if s.G().IsMobileAppType() { 433 maxConvs = s.maxConvLoads 434 num = 30 435 pageBack = 0 436 if netState.IsLimited() || state == keybase1.MobileAppState_FOREGROUND { 437 maxConvs = s.maxLimitedConvLoads 438 } 439 } 440 // Sort big teams convs lower (and by time to tie break) 441 sort.Slice(iboxSyncRes.FilteredConvs, func(i, j int) bool { 442 itype := iboxSyncRes.FilteredConvs[i].GetTeamType() 443 jtype := iboxSyncRes.FilteredConvs[j].GetTeamType() 444 if itype == chat1.TeamType_COMPLEX && jtype != chat1.TeamType_COMPLEX { 445 return false 446 } 447 if jtype == chat1.TeamType_COMPLEX && itype != chat1.TeamType_COMPLEX { 448 return true 449 } 450 return utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[i]) >= utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[j]) 451 }) 452 453 // Dispatch background jobs 454 for _, rc := range iboxSyncRes.FilteredConvs { 455 conv := rc.Conv 456 if expunge, ok := expunges[conv.GetConvID().ConvIDStr()]; ok { 457 // Run expunges on the background loader 458 s.Debug(ctx, "Sync: queueing expunge background loader job: convID: %s", conv.GetConvID()) 459 job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num}, 460 types.ConvLoaderPriorityHighest, types.ConvLoaderUnique, 461 func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) { 462 s.Debug(ctx, "Sync: executing expunge from a sync run: convID: %s", conv.GetConvID()) 463 err := s.G().ConvSource.Expunge(ctx, conv, uid, expunge) 464 if err != nil { 465 s.Debug(ctx, "Sync: failed to expunge: %v", err) 466 } 467 }) 468 if err := s.G().ConvLoader.Queue(ctx, job); err != nil { 469 s.Debug(ctx, "Sync: failed to queue conversation load: %s", err) 470 } 471 queuedConvs++ 472 } else { 473 // If we set maxConvs, then check it now 474 if maxConvs > 0 && (queuedConvs >= maxConvs || !s.shouldUnboxSyncConv(conv)) { 475 continue 476 } 477 s.Debug(ctx, "Sync: queueing background loader job: convID: %s", conv.GetConvID()) 478 // Everything else just queue up here 479 job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num}, 480 types.ConvLoaderPriorityMedium, types.ConvLoaderGeneric, 481 newConvLoaderPagebackHook(s.G(), 0, pageBack)) 482 if err := s.G().ConvLoader.Queue(ctx, job); err != nil { 483 s.Debug(ctx, "Sync: failed to queue conversation load: %s", err) 484 } 485 queuedConvs++ 486 } 487 } 488 } 489 490 return nil 491 } 492 493 func (s *Syncer) RegisterOfflinable(offlinable types.Offlinable) { 494 s.Lock() 495 defer s.Unlock() 496 s.offlinables = append(s.offlinables, offlinable) 497 } 498 499 func (s *Syncer) GetSelectedConversation() chat1.ConversationID { 500 s.lastLoadedLock.Lock() 501 defer s.lastLoadedLock.Unlock() 502 return s.lastLoadedConv 503 } 504 505 func (s *Syncer) IsSelectedConversation(convID chat1.ConversationID) bool { 506 s.lastLoadedLock.Lock() 507 defer s.lastLoadedLock.Unlock() 508 return s.lastLoadedConv.Eq(convID) 509 } 510 511 func (s *Syncer) SelectConversation(ctx context.Context, convID chat1.ConversationID) { 512 s.lastLoadedLock.Lock() 513 defer s.lastLoadedLock.Unlock() 514 s.Debug(ctx, "SelectConversation: setting last loaded conv to: %s", convID) 515 s.lastLoadedConv = convID 516 }