/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/chat/retry.go
403 строки
11 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" "errors" "fmt" "sync" "time" "github.com/keybase/client/go/chat/globals" "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" "github.com/keybase/go-codec/codec" ) type FetchType int const ( InboxLoad FetchType = iota ThreadLoad FullInboxLoad ) const ( fetchInitialInterval = 3 * time.Second fetchMultiplier = 1.5 fetchMaxAttempts = 100 ) type ConversationRetry struct { globals.Contextified utils.DebugLabeler convID chat1.ConversationID tlfID *chat1.TLFID kind FetchType } var _ types.RetryDescription = (*ConversationRetry)(nil) func NewConversationRetry(g *globals.Context, convID chat1.ConversationID, tlfID *chat1.TLFID, kind FetchType) *ConversationRetry { dstr := fmt.Sprintf("ConversationRetry(%s,%v)", convID, kind) return &ConversationRetry{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), dstr, false), convID: convID, tlfID: tlfID, kind: kind, } } func (c *ConversationRetry) String() string { return fmt.Sprintf("%s:%v", c.convID, c.kind) } func (c *ConversationRetry) RekeyFixable(ctx context.Context, tlfID chat1.TLFID) bool { return c.tlfID != nil && c.tlfID.Eq(tlfID) } func (c *ConversationRetry) SendStale(ctx context.Context, uid gregor1.UID) { supdates := []chat1.ConversationStaleUpdate{{ ConvID: c.convID, UpdateType: chat1.StaleUpdateType_NEWACTIVITY, }} c.G().Syncer.SendChatStaleNotifications(ctx, uid, supdates, false) } func (c *ConversationRetry) Fix(ctx context.Context, uid gregor1.UID) error { if c.kind == ThreadLoad { return c.fixThreadFetch(ctx, uid) } return c.fixInboxFetch(ctx, uid) } func (c *ConversationRetry) fixInboxFetch(ctx context.Context, uid gregor1.UID) error { c.Debug(ctx, "fixInboxFetch: retrying conversation") // Reload this conversation and hope it works inbox, _, err := c.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil, &chat1.GetInboxLocalQuery{ ConvIDs: []chat1.ConversationID{c.convID}, }) if err != nil { c.Debug(ctx, "fixInboxFetch: failed to read inbox: msg: %s", err.Error()) return err } if len(inbox.Convs) != 1 { c.Debug(ctx, "fixInboxFetch: unusual number of results for Read call: len: %d", len(inbox.Convs)) return errors.New("inbox fetch failed: unusual number of conversation returned") } conv := inbox.Convs[0] if conv.Error == nil { c.Debug(ctx, "fixInboxFetch: fixed convID: %s", conv.GetConvID()) return nil } c.Debug(ctx, "fixInboxFetch: convID failed again: msg: %s typ: %v", conv.Error.Message, conv.Error.Typ) return fmt.Errorf("inbox fetch failed: %s", conv.Error.Message) } func (c *ConversationRetry) fixThreadFetch(ctx context.Context, uid gregor1.UID) error { c.Debug(ctx, "fixThreadFetch: retrying conversation") // Attempt a pull of 50 messages to simulate whatever request got the // conversation in this queue. _, err := c.G().ConvSource.Pull(ctx, c.convID, uid, chat1.GetThreadReason_FIXRETRY, nil, nil, &chat1.Pagination{ Num: 50, }) if err == nil { c.Debug(ctx, "fixThreadFetch: fixed") return nil } c.Debug(ctx, "fixThreadFetch: convID failed again: msg: %s", err.Error()) return err } type FullInboxRetry struct { globals.Contextified utils.DebugLabeler query *chat1.GetInboxLocalQuery } var _ types.RetryDescription = (*FullInboxRetry)(nil) func NewFullInboxRetry(g *globals.Context, query *chat1.GetInboxLocalQuery) FullInboxRetry { return FullInboxRetry{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "FullInboxRetry", false), query: query, } } func (f FullInboxRetry) String() string { qstr := "<empty>" if f.query != nil { mh := codec.MsgpackHandle{WriteExt: true} var data []byte enc := codec.NewEncoderBytes(&data, &mh) err := enc.Encode(*f.query) if err != nil { panic(err) } qstr = hex.EncodeToString(data) } pstr := "<empty>" return qstr + pstr } func (f FullInboxRetry) RekeyFixable(ctx context.Context, tlfID chat1.TLFID) bool { return false } func (f FullInboxRetry) SendStale(ctx context.Context, uid gregor1.UID) { f.G().Syncer.SendChatStaleNotifications(ctx, uid, nil, true) } func (f FullInboxRetry) Fix(ctx context.Context, uid gregor1.UID) error { query, _, err := f.G().InboxSource.GetInboxQueryLocalToRemote(ctx, f.query) if err != nil { f.Debug(ctx, "Fix: failed to convert query: %s", err.Error()) return err } _, err = f.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, query) if err != nil { f.Debug(ctx, "Fix: failed to load again: %d", err.Error()) } return nil } type retrierControl struct { desc types.RetryDescription forceCh chan struct{} shutdownCh chan struct{} } func newRetrierControl(desc types.RetryDescription) *retrierControl { return &retrierControl{ desc: desc, forceCh: make(chan struct{}, 1), shutdownCh: make(chan struct{}, 1), } } func (c *retrierControl) Shutdown() { select { case c.shutdownCh <- struct{}{}: default: } } func (c *retrierControl) Force() { select { case c.forceCh <- struct{}{}: default: } } // FetchRetrier is responsible for tracking any nonblock fetch failures, and retrying // them automatically. type FetchRetrier struct { globals.Contextified utils.DebugLabeler sync.Mutex retriers map[string]*retrierControl clock clockwork.Clock offline, running bool } var _ types.FetchRetrier = (*FetchRetrier)(nil) func NewFetchRetrier(g *globals.Context) *FetchRetrier { f := &FetchRetrier{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "FetchRetrier", false), clock: clockwork.NewRealClock(), retriers: make(map[string]*retrierControl), } return f } // SetClock sets a custom clock for testing. func (f *FetchRetrier) SetClock(clock clockwork.Clock) { f.clock = clock } func (f *FetchRetrier) key(uid gregor1.UID, desc types.RetryDescription) string { return fmt.Sprintf("%s:%s", uid, desc) } // nextAttemptTime calculates the next try for a given retry item. It uses an exponential // decay calculation. func (f *FetchRetrier) nextAttemptTime(attempts int, lastAttempt time.Time) time.Time { wait := time.Duration(float64(attempts) * fetchMultiplier * float64(fetchInitialInterval)) return lastAttempt.Add(wait) } func (f *FetchRetrier) spawnRetrier(ctx context.Context, uid gregor1.UID, desc types.RetryDescription, control *retrierControl, ) { attempts := 1 nextTime := f.nextAttemptTime(attempts, f.clock.Now()) ctx = globals.BackgroundChatCtx(ctx, f.G()) go func() { for { select { case <-f.clock.AfterTime(nextTime): // Only attempts if we are online. Otherwise just retry // at the same interval that we used last time. if !f.offline { f.Debug(ctx, "spawnRetrier: retrying after time: desc: %s", desc) if err := desc.Fix(ctx, uid); err == nil { f.Lock() delete(f.retriers, f.key(uid, desc)) f.Unlock() desc.SendStale(ctx, uid) return } } case <-control.forceCh: f.Debug(ctx, "spawnRetrier: retrying (forced): desc: %s", desc) if err := desc.Fix(ctx, uid); err == nil { f.Lock() delete(f.retriers, f.key(uid, desc)) f.Unlock() desc.SendStale(ctx, uid) return } case <-control.shutdownCh: f.Lock() defer f.Unlock() f.Debug(ctx, "spawnRetrier: shutdown received, going down: desc: %s", desc) delete(f.retriers, f.key(uid, desc)) return } attempts++ if attempts > fetchMaxAttempts { f.Debug(ctx, "spawnRetrier: max attempts reached, bailing: desc: %s", desc) control.Shutdown() } nextTime = f.nextAttemptTime(attempts, f.clock.Now()) f.Debug(ctx, "spawnRetrier: attempts: %d next: %v desc: %s", attempts, nextTime, desc) } }() } // Failure indicates a failure of type kind has happened when loading a conversation. func (f *FetchRetrier) Failure(ctx context.Context, uid gregor1.UID, desc types.RetryDescription) { defer f.Trace(ctx, nil, "Failure(%s)", desc)() f.Lock() defer f.Unlock() if !f.running { f.Debug(ctx, "Failure: not starting new retrier, not running") return } key := f.key(uid, desc) if _, ok := f.retriers[key]; !ok { f.Debug(ctx, "Failure: spawning new retrier: desc: %s", desc) control := newRetrierControl(desc) f.retriers[key] = control f.spawnRetrier(ctx, uid, desc, control) } } // Success indicates a success of type kind loading a conversation. This effectively removes // that conversation from the retry queue. func (f *FetchRetrier) Success(ctx context.Context, uid gregor1.UID, desc types.RetryDescription) { defer f.Trace(ctx, nil, "Success(%s)", desc)() f.Lock() defer f.Unlock() key := f.key(uid, desc) if control, ok := f.retriers[key]; ok { control.Shutdown() } } // Connected is called when a connection to the chat server is established, and forces a // pass over the retry queue func (f *FetchRetrier) Connected(ctx context.Context) { defer f.Trace(ctx, nil, "Connected")() f.Lock() defer f.Unlock() f.offline = false for _, control := range f.retriers { control.Force() } } // Disconnected is called when we lose connection to the chat server, and pauses attempts // on the retry queue. func (f *FetchRetrier) Disconnected(ctx context.Context) { f.Lock() defer f.Unlock() f.offline = true } // IsOffline returns if the module thinks we are connected to the chat server. func (f *FetchRetrier) IsOffline(ctx context.Context) bool { f.Lock() defer f.Unlock() return f.offline } // Force forces a run of the retry loop. func (f *FetchRetrier) Force(ctx context.Context) { defer f.Trace(ctx, nil, "Force")() f.Lock() defer f.Unlock() for _, control := range f.retriers { control.Force() } } func (f *FetchRetrier) Rekey(ctx context.Context, name string, membersType chat1.ConversationMembersType, public bool, ) { nameInfo, err := CreateNameInfoSource(ctx, f.G(), membersType).LookupID(ctx, name, public) if err != nil { f.Debug(ctx, "Rekey: failed to load name info for: %s msg %s", name, err) return } var forces []*retrierControl f.Lock() for _, control := range f.retriers { if control.desc.RekeyFixable(ctx, nameInfo.ID) { forces = append(forces, control) } } f.Unlock() for _, force := range forces { f.Debug(ctx, "Rekey: forcing: %s", force.desc) force.Force() } } func (f *FetchRetrier) Stop(ctx context.Context) chan struct{} { defer f.Trace(ctx, nil, "Shutdown")() f.Lock() defer f.Unlock() f.running = false for _, control := range f.retriers { control.Shutdown() } ch := make(chan struct{}) close(ch) return ch } func (f *FetchRetrier) Start(ctx context.Context, uid gregor1.UID) { f.Lock() defer f.Unlock() f.running = true }