/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/chat/sync.go
523 строки
17 KB
zoom-ua
remove golang.org/x/net/context in favor of context (#28708)
17 дек 2025, 18:24
Не верифицирован
17 дек 2025, 18:24
67b0ffe
Код
Авторство
О чём код?
package chat import ( "context" "encoding/hex" "sort" "sync" "time" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/clockwork" ) type Syncer struct { globals.Contextified utils.DebugLabeler sync.Mutex isConnected bool offlinables []types.Offlinable notificationLock sync.Mutex lastLoadedLock sync.Mutex clock clockwork.Clock sendDelay time.Duration shutdownCh chan struct{} fullReloadCh chan gregor1.UID flushCh chan struct{} notificationQueue map[string][]chat1.ConversationStaleUpdate fullReload map[string]bool lastLoadedConv chat1.ConversationID maxLimitedConvLoads int maxConvLoads int } func NewSyncer(g *globals.Context) *Syncer { s := &Syncer{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Syncer", false), isConnected: false, clock: clockwork.NewRealClock(), shutdownCh: make(chan struct{}), fullReloadCh: make(chan gregor1.UID), flushCh: make(chan struct{}), notificationQueue: make(map[string][]chat1.ConversationStaleUpdate), fullReload: make(map[string]bool), sendDelay: time.Millisecond * 1000, maxLimitedConvLoads: 3, maxConvLoads: 10, } go s.sendNotificationLoop() return s } func (s *Syncer) SetClock(clock clockwork.Clock) { s.clock = clock } func (s *Syncer) Shutdown() { s.Debug(context.Background(), "shutting down") close(s.shutdownCh) } func (s *Syncer) dedupUpdates(updates []chat1.ConversationStaleUpdate) (res []chat1.ConversationStaleUpdate) { m := make(map[chat1.ConvIDStr]chat1.ConversationStaleUpdate) for _, update := range updates { if existing, ok := m[update.ConvID.ConvIDStr()]; ok { switch existing.UpdateType { case chat1.StaleUpdateType_CLEAR: // do nothing, existing is already clearing case chat1.StaleUpdateType_NEWACTIVITY: m[update.ConvID.ConvIDStr()] = update } } else { m[update.ConvID.ConvIDStr()] = update } } for _, update := range m { res = append(res, update) } return res } func (s *Syncer) sendNotificationsOnce() { s.notificationLock.Lock() defer s.notificationLock.Unlock() // Broadcast full reloads for uid := range s.fullReload { s.Debug(context.Background(), "flushing full reload: uid: %s", uid) b, _ := hex.DecodeString(uid) s.G().ActivityNotifier.InboxStale(context.Background(), gregor1.UID(b)) } s.fullReload = make(map[string]bool) // Broadcast conversation stales for uid, updates := range s.notificationQueue { updates = s.dedupUpdates(updates) b, _ := hex.DecodeString(uid) s.Debug(context.Background(), "flushing notifications: uid: %s len: %d", uid, len(updates)) for _, update := range updates { s.Debug(context.Background(), "flushing: uid: %s convID: %s type: %v", uid, update.ConvID, update.UpdateType) } s.G().ActivityNotifier.ThreadsStale(context.Background(), gregor1.UID(b), updates) } s.notificationQueue = make(map[string][]chat1.ConversationStaleUpdate) } func (s *Syncer) sendNotificationLoop() { s.Debug(context.Background(), "starting notification loop") for { select { case <-s.shutdownCh: return case uid := <-s.fullReloadCh: s.notificationLock.Lock() s.fullReload[uid.String()] = true delete(s.notificationQueue, uid.String()) s.notificationLock.Unlock() s.sendNotificationsOnce() case <-s.clock.After(s.sendDelay): s.sendNotificationsOnce() case <-s.flushCh: s.sendNotificationsOnce() } } } func (s *Syncer) SendChatStaleNotifications(ctx context.Context, uid gregor1.UID, updates []chat1.ConversationStaleUpdate, immediate bool, ) { if len(updates) == 0 { s.Debug(ctx, "sending inbox stale message") s.fullReloadCh <- uid } else { s.Debug(ctx, "sending thread stale messages: len: %d", len(updates)) for _, update := range updates { s.Debug(ctx, "sending thread stale message: convID: %s type: %v", update.ConvID, update.UpdateType) } s.notificationLock.Lock() if !s.fullReload[uid.String()] { s.notificationQueue[uid.String()] = append(s.notificationQueue[uid.String()], updates...) } s.notificationLock.Unlock() if immediate { s.flushCh <- struct{}{} } } } func (s *Syncer) isServerInboxClear(ctx context.Context, _ *storage.Inbox, srvVers int) bool { if _, err := s.G().ServerCacheVersions.MatchInbox(ctx, srvVers); err != nil { s.Debug(ctx, "isServerInboxClear: inbox server version match error: %s", err.Error()) return true } return false } func (s *Syncer) IsConnected(ctx context.Context) bool { s.Lock() defer s.Unlock() return s.isConnected } func (s *Syncer) Connected(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID, syncRes *chat1.SyncChatRes, ) (err error) { ctx = globals.CtxAddLogTags(ctx, s.G()) defer s.Trace(ctx, &err, "Connected")() s.Lock() s.isConnected = true // Let the Offlinables know that we are back online for _, o := range s.offlinables { o.Connected(ctx) } s.Unlock() // Run sync against the server return s.Sync(ctx, cli, uid, syncRes) } func (s *Syncer) Disconnected(ctx context.Context) { defer s.Trace(ctx, nil, "Disconnected")() s.Lock() s.isConnected = false // Let the Offlinables know of connection state change for _, o := range s.offlinables { o.Disconnected(ctx) } s.Unlock() } func (s *Syncer) handleMembersTypeChanged(ctx context.Context, uid gregor1.UID, convIDs []chat1.ConversationID, ) { // Clear caches from members type changed convos for _, convID := range convIDs { s.Debug(ctx, "handleMembersTypeChanged: clearing message cache: %s", convID) err := s.G().ConvSource.Clear(ctx, convID, uid, nil) if err != nil { s.Debug(ctx, "handleMembersTypeChanged: erroring clearing conv: %+v", err) } } } func (s *Syncer) handleFilteredConvs(ctx context.Context, uid gregor1.UID, syncConvs []chat1.Conversation, filteredConvs []types.RemoteConversation, ) { fmap := make(map[chat1.ConvIDStr]bool) for _, fconv := range filteredConvs { fmap[fconv.Conv.GetConvID().ConvIDStr()] = true } // If any sync convs are not in the filtered list, let's blow away their local storage for _, sconv := range syncConvs { if !fmap[sconv.GetConvID().ConvIDStr()] { s.Debug(ctx, "handleFilteredConvs: conv filtered from inbox, removing cache: convID: %s memberStatus: %v existence: %v", sconv.GetConvID(), sconv.ReaderInfo.Status, sconv.Metadata.Existence) err := s.G().ConvSource.Clear(ctx, sconv.GetConvID(), uid, nil) if err != nil { s.Debug(ctx, "handleFilteredCovs: erroring clearing conv: %+v", err) } } } } func (s *Syncer) maxSyncUnboxConvs() int { if s.G().IsMobileAppType() { return 8 } return 100 } func (s *Syncer) getShouldUnboxSyncConvMap(ctx context.Context, convs []chat1.Conversation, topicNameChanged []chat1.ConversationID, ) (m map[chat1.ConvIDStr]bool) { m = make(map[chat1.ConvIDStr]bool) for _, t := range topicNameChanged { m[t.ConvIDStr()] = true } rconvs := utils.RemoteConvs(convs) sort.Slice(rconvs, func(i, j int) bool { return utils.GetConvPriorityScore(rconvs[i]) >= utils.GetConvPriorityScore(rconvs[j]) }) maxConvs := s.maxSyncUnboxConvs() for _, conv := range rconvs { if len(m) >= maxConvs { s.Debug(ctx, "getShouldUnboxSyncConvMap: max sync convs reached, not including any others") break } if m[conv.ConvIDStr] { continue } if s.shouldUnboxSyncConv(conv.Conv) { m[conv.ConvIDStr] = true } } return m } func (s *Syncer) shouldUnboxSyncConv(conv chat1.Conversation) bool { // only chat on mobile if s.G().IsMobileAppType() && conv.GetTopicType() != chat1.TopicType_CHAT { return false } // Skips convs we don't care for. switch conv.Metadata.Status { case chat1.ConversationStatus_BLOCKED, chat1.ConversationStatus_IGNORED, chat1.ConversationStatus_REPORTED: return false } // Only let through ACTIVE/PREVIEW convs. if conv.ReaderInfo != nil { switch conv.ReaderInfo.Status { case chat1.ConversationMemberStatus_ACTIVE, chat1.ConversationMemberStatus_PREVIEW: default: return false } } switch conv.GetMembersType() { case chat1.ConversationMembersType_TEAM: // include if this is a simple team or we are currently viewing the // conv. return conv.GetTopicType() == chat1.TopicType_KBFSFILEEDIT || conv.Metadata.TeamType != chat1.TeamType_COMPLEX || conv.GetConvID().Eq(s.GetSelectedConversation()) default: return true } } func (s *Syncer) notifyIncrementalSync(ctx context.Context, uid gregor1.UID, allConvs []chat1.Conversation, shouldUnboxMap map[chat1.ConvIDStr]bool, ) { if len(allConvs) == 0 { s.Debug(ctx, "notifyIncrementalSync: no conversations given, sending a current result") s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, chat1.NewChatSyncResultWithCurrent()) return } itemsByTopicType := make(map[chat1.TopicType][]chat1.ChatSyncIncrementalConv) for _, c := range allConvs { var md *types.RemoteConversationMetadata rc, err := utils.GetUnverifiedConv(ctx, s.G(), uid, c.GetConvID(), types.InboxSourceDataSourceLocalOnly) if err == nil { md = rc.LocalMetadata } rc = utils.RemoteConv(c) rc.LocalMetadata = md itemsByTopicType[c.GetTopicType()] = append(itemsByTopicType[c.GetTopicType()], chat1.ChatSyncIncrementalConv{ Conv: utils.PresentRemoteConversation(ctx, s.G(), uid, rc), ShouldUnbox: shouldUnboxMap[c.GetConvID().ConvIDStr()], }) } for _, topicType := range chat1.TopicTypeMap { if topicType == chat1.TopicType_NONE { continue } s.G().ActivityNotifier.InboxSynced(ctx, uid, topicType, chat1.NewChatSyncResultWithIncremental(chat1.ChatSyncIncrementalInfo{ Items: itemsByTopicType[topicType], })) } } func (s *Syncer) Sync(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID, syncRes *chat1.SyncChatRes, ) (err error) { defer s.Trace(ctx, &err, "Sync")() s.Lock() if !s.isConnected { defer s.Unlock() s.Debug(ctx, "Sync: aborting because currently offline") return OfflineError{} } s.Unlock() // Grab current on disk version ibox := storage.NewInbox(s.G()) vers, err := ibox.Version(ctx, uid) if err != nil { return err } srvVers, err := ibox.ServerVersion(ctx, uid) if err != nil { return err } s.Debug(ctx, "Sync: current inbox version: %v server version: %d", vers, srvVers) if syncRes == nil { // Run the sync call on the server to see how current our local copy is syncRes = new(chat1.SyncChatRes) if *syncRes, err = cli.SyncChat(ctx, chat1.SyncChatArg{ Vers: vers, SummarizeMaxMsgs: true, ParticipantsMode: chat1.InboxParticipantsMode_SKIP_TEAMS, }); err != nil { s.Debug(ctx, "Sync: failed to sync inbox: %s", err.Error()) return err } } else { s.Debug(ctx, "Sync: skipping sync call, data provided") } // Set new server versions if err = s.G().ServerCacheVersions.Set(ctx, syncRes.CacheVers); err != nil { s.Debug(ctx, "Sync: failed to set new server versions: %s", err.Error()) } // Process what the server has told us to do with the local inbox copy rtyp, err := syncRes.InboxRes.Typ() if err != nil { s.Debug(ctx, "Sync: strange type from SyncInbox: %s", err.Error()) return err } // Check if the server has cleared the inbox if s.isServerInboxClear(ctx, ibox, srvVers) { rtyp = chat1.SyncInboxResType_CLEAR } switch rtyp { case chat1.SyncInboxResType_CLEAR: s.Debug(ctx, "Sync: version out of date, clearing inbox: %v", vers) if err = ibox.Clear(ctx, uid); err != nil { s.Debug(ctx, "Sync: failed to clear inbox: %s", err.Error()) } // Send notifications for a full clear s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, chat1.NewChatSyncResultWithClear()) case chat1.SyncInboxResType_CURRENT: s.Debug(ctx, "Sync: version is current, standing pat: %v", vers) s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, chat1.NewChatSyncResultWithCurrent()) case chat1.SyncInboxResType_INCREMENTAL: incr := syncRes.InboxRes.Incremental() s.Debug(ctx, "Sync: version out of date, but can incrementally sync: old vers: %v vers: %v convs: %d", vers, incr.Vers, len(incr.Convs)) var iboxSyncRes types.InboxSyncRes expunges := make(map[chat1.ConvIDStr]chat1.Expunge) if iboxSyncRes, err = s.G().InboxSource.Sync(ctx, uid, incr.Vers, incr.Convs); err != nil { s.Debug(ctx, "Sync: failed to sync conversations to inbox: %s", err.Error()) // Send notifications for a full clear s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE, chat1.NewChatSyncResultWithClear()) } else { s.handleMembersTypeChanged(ctx, uid, iboxSyncRes.MembersTypeChanged) s.handleFilteredConvs(ctx, uid, incr.Convs, iboxSyncRes.FilteredConvs) for _, expunge := range iboxSyncRes.Expunges { expunges[expunge.ConvID.ConvIDStr()] = expunge.Expunge } // Send notifications for a successful partial sync shouldUnboxMap := s.getShouldUnboxSyncConvMap(ctx, incr.Convs, iboxSyncRes.TopicNameChanged) s.notifyIncrementalSync(ctx, uid, incr.Convs, shouldUnboxMap) } // The idea here is to limit the amount of work we do with the // background conversation loader on mobile. If we are on a cell // connection, or if we just came into the foreground, limit the number // of conversations we queue up for background loading. var queuedConvs, maxConvs int pageBack := 3 num := 50 netState := s.G().MobileNetState.State() state := s.G().MobileAppState.State() if s.G().IsMobileAppType() { maxConvs = s.maxConvLoads num = 30 pageBack = 0 if netState.IsLimited() || state == keybase1.MobileAppState_FOREGROUND { maxConvs = s.maxLimitedConvLoads } } // Sort big teams convs lower (and by time to tie break) sort.Slice(iboxSyncRes.FilteredConvs, func(i, j int) bool { itype := iboxSyncRes.FilteredConvs[i].GetTeamType() jtype := iboxSyncRes.FilteredConvs[j].GetTeamType() if itype == chat1.TeamType_COMPLEX && jtype != chat1.TeamType_COMPLEX { return false } if jtype == chat1.TeamType_COMPLEX && itype != chat1.TeamType_COMPLEX { return true } return utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[i]) >= utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[j]) }) // Dispatch background jobs for _, rc := range iboxSyncRes.FilteredConvs { conv := rc.Conv if expunge, ok := expunges[conv.GetConvID().ConvIDStr()]; ok { // Run expunges on the background loader s.Debug(ctx, "Sync: queueing expunge background loader job: convID: %s", conv.GetConvID()) job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num}, types.ConvLoaderPriorityHighest, types.ConvLoaderUnique, func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) { s.Debug(ctx, "Sync: executing expunge from a sync run: convID: %s", conv.GetConvID()) err := s.G().ConvSource.Expunge(ctx, conv, uid, expunge) if err != nil { s.Debug(ctx, "Sync: failed to expunge: %v", err) } }) if err := s.G().ConvLoader.Queue(ctx, job); err != nil { s.Debug(ctx, "Sync: failed to queue conversation load: %s", err) } queuedConvs++ } else { // If we set maxConvs, then check it now if maxConvs > 0 && (queuedConvs >= maxConvs || !s.shouldUnboxSyncConv(conv)) { continue } s.Debug(ctx, "Sync: queueing background loader job: convID: %s", conv.GetConvID()) // Everything else just queue up here job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num}, types.ConvLoaderPriorityMedium, types.ConvLoaderGeneric, newConvLoaderPagebackHook(s.G(), 0, pageBack)) if err := s.G().ConvLoader.Queue(ctx, job); err != nil { s.Debug(ctx, "Sync: failed to queue conversation load: %s", err) } queuedConvs++ } } } return nil } func (s *Syncer) RegisterOfflinable(offlinable types.Offlinable) { s.Lock() defer s.Unlock() s.offlinables = append(s.offlinables, offlinable) } func (s *Syncer) GetSelectedConversation() chat1.ConversationID { s.lastLoadedLock.Lock() defer s.lastLoadedLock.Unlock() return s.lastLoadedConv } func (s *Syncer) IsSelectedConversation(convID chat1.ConversationID) bool { s.lastLoadedLock.Lock() defer s.lastLoadedLock.Unlock() return s.lastLoadedConv.Eq(convID) } func (s *Syncer) SelectConversation(ctx context.Context, convID chat1.ConversationID) { s.lastLoadedLock.Lock() defer s.lastLoadedLock.Unlock() s.Debug(ctx, "SelectConversation: setting last loaded conv to: %s", convID) s.lastLoadedConv = convID }