github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/convloader.go (about)

     1  package chat
     2  
     3  import (
     4  	"container/list"
     5  	"errors"
     6  	"sync"
     7  	"time"
     8  
     9  	"golang.org/x/net/context"
    10  	"golang.org/x/sync/errgroup"
    11  
    12  	"github.com/keybase/client/go/chat/globals"
    13  	"github.com/keybase/client/go/chat/storage"
    14  	"github.com/keybase/client/go/chat/types"
    15  	"github.com/keybase/client/go/chat/utils"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/chat1"
    18  	"github.com/keybase/client/go/protocol/gregor1"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	"github.com/keybase/clockwork"
    21  )
    22  
    23  const (
    24  	bgLoaderMaxAttempts = 10
    25  	bgLoaderInitDelay   = 100 * time.Millisecond
    26  	bgLoaderErrDelay    = 300 * time.Millisecond
    27  )
    28  
    29  type clTask struct {
    30  	job           types.ConvLoaderJob
    31  	attempt       int
    32  	lastAttemptAt time.Time
    33  }
    34  
    35  type jobQueue struct {
    36  	sync.Mutex
    37  	queue    *list.List
    38  	waitChs  []chan struct{}
    39  	queueMap map[string]bool
    40  	maxSize  int
    41  }
    42  
    43  func newJobQueue(maxSize int) *jobQueue {
    44  	return &jobQueue{
    45  		queue:    list.New(),
    46  		queueMap: make(map[string]bool),
    47  		maxSize:  maxSize,
    48  	}
    49  }
    50  
    51  func (j *jobQueue) Wait() <-chan struct{} {
    52  	j.Lock()
    53  	defer j.Unlock()
    54  	if j.queue.Len() == 0 {
    55  		ch := make(chan struct{})
    56  		j.waitChs = append(j.waitChs, ch)
    57  		return ch
    58  	}
    59  	ch := make(chan struct{})
    60  	close(ch)
    61  	return ch
    62  }
    63  
    64  func (j *jobQueue) Push(task clTask) (queued bool, err error) {
    65  	j.Lock()
    66  	defer j.Unlock()
    67  	if j.queue.Len() >= j.maxSize {
    68  		return false, errors.New("job queue full")
    69  	}
    70  	defer func() {
    71  		if !queued {
    72  			return
    73  		}
    74  		// Notify waiters we have some stuff for them now
    75  		for _, w := range j.waitChs {
    76  			close(w)
    77  		}
    78  		j.waitChs = nil
    79  	}()
    80  	if task.job.Uniqueness == types.ConvLoaderGeneric && j.queueMap[task.job.String()] {
    81  		return false, nil
    82  	}
    83  	j.queueMap[task.job.String()] = true
    84  	for e := j.queue.Front(); e != nil; e = e.Next() {
    85  		eval := e.Value.(clTask)
    86  		if task.job.HigherPriorityThan(eval.job) {
    87  			j.queue.InsertBefore(task, e)
    88  			return true, nil
    89  		}
    90  	}
    91  	j.queue.PushBack(task)
    92  	return true, nil
    93  }
    94  
    95  func (j *jobQueue) PopFront() (res clTask, ok bool) {
    96  	j.Lock()
    97  	defer j.Unlock()
    98  	if j.queue.Len() == 0 {
    99  		return res, false
   100  	}
   101  	el := j.queue.Front()
   102  	res = el.Value.(clTask)
   103  	j.queue.Remove(el)
   104  	delete(j.queueMap, res.job.String())
   105  	return res, true
   106  }
   107  
   108  type activeLoad struct {
   109  	Ctx      context.Context
   110  	CancelFn context.CancelFunc
   111  }
   112  
   113  type BackgroundConvLoader struct {
   114  	globals.Contextified
   115  	utils.DebugLabeler
   116  	sync.Mutex
   117  
   118  	uid           gregor1.UID
   119  	started       bool
   120  	queue         *jobQueue
   121  	stopCh        chan struct{}
   122  	suspendCh     chan chan struct{}
   123  	resumeCh      chan struct{}
   124  	loadCh        chan *clTask
   125  	identNotifier types.IdentifyNotifier
   126  	eg            errgroup.Group
   127  
   128  	clock      clockwork.Clock
   129  	resumeWait time.Duration
   130  	loadWait   time.Duration
   131  
   132  	activeLoads  map[string]activeLoad
   133  	suspendCount int
   134  
   135  	// for testing, make this and can check conv load successes
   136  	loads                 chan chat1.ConversationID
   137  	testingNameInfoSource types.NameInfoSource
   138  	appStateCh            chan struct{}
   139  }
   140  
   141  var _ types.ConvLoader = (*BackgroundConvLoader)(nil)
   142  
   143  func NewBackgroundConvLoader(g *globals.Context) *BackgroundConvLoader {
   144  	b := &BackgroundConvLoader{
   145  		Contextified:  globals.NewContextified(g),
   146  		DebugLabeler:  utils.NewDebugLabeler(g.ExternalG(), "BackgroundConvLoader", false),
   147  		stopCh:        make(chan struct{}),
   148  		suspendCh:     make(chan chan struct{}, 10),
   149  		identNotifier: NewCachingIdentifyNotifier(g),
   150  		clock:         clockwork.NewRealClock(),
   151  		resumeWait:    time.Second,
   152  		loadWait:      time.Second,
   153  		activeLoads:   make(map[string]activeLoad),
   154  	}
   155  	b.identNotifier.ResetOnGUIConnect()
   156  	b.newQueue()
   157  	go func() { _ = b.monitorAppState() }()
   158  
   159  	return b
   160  }
   161  
   162  func (b *BackgroundConvLoader) addActiveLoadLocked(al activeLoad) (key string) {
   163  	key = libkb.RandStringB64(3)
   164  	b.activeLoads[key] = al
   165  	return key
   166  }
   167  
   168  func (b *BackgroundConvLoader) removeActiveLoadLocked(key string) {
   169  	delete(b.activeLoads, key)
   170  }
   171  
   172  func (b *BackgroundConvLoader) monitorAppState() error {
   173  	ctx := context.Background()
   174  	b.Debug(ctx, "monitorAppState: starting up")
   175  
   176  	suspended := false
   177  	state := keybase1.MobileAppState_FOREGROUND
   178  	for {
   179  		state = <-b.G().MobileAppState.NextUpdate(&state)
   180  		switch state {
   181  		case keybase1.MobileAppState_FOREGROUND, keybase1.MobileAppState_BACKGROUNDACTIVE:
   182  			b.Debug(ctx, "monitorAppState: active state: %v", state)
   183  			// Only resume if we had suspended earlier (frontend can spam us with these)
   184  			if suspended {
   185  				b.Debug(ctx, "monitorAppState: resuming load thread")
   186  				b.Resume(ctx)
   187  				suspended = false
   188  			}
   189  		case keybase1.MobileAppState_BACKGROUND:
   190  			b.Debug(ctx, "monitorAppState: backgrounded, suspending load thread")
   191  			if !suspended {
   192  				b.Suspend(ctx)
   193  				suspended = true
   194  			}
   195  		}
   196  		if b.appStateCh != nil {
   197  			b.appStateCh <- struct{}{}
   198  		}
   199  	}
   200  }
   201  
   202  func (b *BackgroundConvLoader) Start(ctx context.Context, uid gregor1.UID) {
   203  	b.Lock()
   204  	defer b.Unlock()
   205  
   206  	if b.G().GetEnv().GetDisableBgConvLoader() {
   207  		b.Debug(ctx, "BackgroundConvLoader disabled, aborting Start")
   208  		return
   209  	}
   210  	b.Debug(ctx, "Start")
   211  	if b.started {
   212  		close(b.stopCh)
   213  		b.stopCh = make(chan struct{})
   214  	}
   215  	b.newQueue()
   216  	b.started = true
   217  	b.uid = uid
   218  	b.eg.Go(func() error { return b.loop(uid, b.stopCh) })
   219  	b.eg.Go(func() error { return b.loadLoop(uid, b.stopCh) })
   220  }
   221  
   222  func (b *BackgroundConvLoader) Stop(ctx context.Context) chan struct{} {
   223  	b.Lock()
   224  	defer b.Unlock()
   225  	b.Debug(ctx, "Stop")
   226  	b.cancelActiveLoadsLocked()
   227  	ch := make(chan struct{})
   228  	if b.started {
   229  		b.started = false
   230  		close(b.stopCh)
   231  		b.stopCh = make(chan struct{})
   232  		go func() {
   233  			_ = b.eg.Wait()
   234  			close(ch)
   235  		}()
   236  	} else {
   237  		close(ch)
   238  	}
   239  	return ch
   240  }
   241  
   242  type bgOperationKey int
   243  
   244  var bgOpKey bgOperationKey
   245  
   246  func (b *BackgroundConvLoader) makeConvLoaderContext(ctx context.Context) context.Context {
   247  	return context.WithValue(ctx, bgOpKey, true)
   248  }
   249  
   250  func (b *BackgroundConvLoader) isConvLoaderContext(ctx context.Context) bool {
   251  	val := ctx.Value(bgOpKey)
   252  	if _, ok := val.(bool); ok {
   253  		return true
   254  	}
   255  	return false
   256  }
   257  
   258  func (b *BackgroundConvLoader) setTestingNameInfoSource(ni types.NameInfoSource) {
   259  	b.Debug(context.TODO(), "setTestingNameInfoSource: setting to %T", ni)
   260  	b.testingNameInfoSource = ni
   261  }
   262  
   263  func (b *BackgroundConvLoader) Queue(ctx context.Context, job types.ConvLoaderJob) error {
   264  	// allow high priority to be queued even in the bkg loader context. Often times, this is something like
   265  	// an ephemeral purge which we don't want to block.
   266  	if job.Priority != types.ConvLoaderPriorityHighest && b.isConvLoaderContext(ctx) {
   267  		b.Debug(ctx, "Queue: refusing to queue in background loader context: convID: %s", job)
   268  		return nil
   269  	}
   270  	return b.enqueue(ctx, clTask{job: job})
   271  }
   272  
   273  func (b *BackgroundConvLoader) cancelActiveLoadsLocked() (canceled bool) {
   274  	for _, activeLoad := range b.activeLoads {
   275  		select {
   276  		case <-activeLoad.Ctx.Done():
   277  			b.Debug(activeLoad.Ctx, "Suspend: active load already canceled")
   278  		default:
   279  			b.Debug(activeLoad.Ctx, "Suspend: canceling active load")
   280  			activeLoad.CancelFn()
   281  			canceled = true
   282  		}
   283  	}
   284  	return canceled
   285  }
   286  
   287  func (b *BackgroundConvLoader) Suspend(ctx context.Context) (canceled bool) {
   288  	defer b.Trace(ctx, nil, "Suspend")()
   289  	b.Lock()
   290  	defer b.Unlock()
   291  	if !b.started {
   292  		return false
   293  	}
   294  	if b.suspendCount == 0 {
   295  		b.Debug(ctx, "Suspend: sending on suspendCh")
   296  		b.resumeCh = make(chan struct{})
   297  		select {
   298  		case b.suspendCh <- b.resumeCh:
   299  		default:
   300  			b.Debug(ctx, "Suspend: failed to suspend loop")
   301  		}
   302  	}
   303  	b.suspendCount++
   304  	return b.cancelActiveLoadsLocked()
   305  }
   306  
   307  func (b *BackgroundConvLoader) Resume(ctx context.Context) bool {
   308  	defer b.Trace(ctx, nil, "Resume")()
   309  	b.Lock()
   310  	defer b.Unlock()
   311  	if b.suspendCount > 0 {
   312  		b.suspendCount--
   313  		if b.suspendCount == 0 && b.resumeCh != nil {
   314  			b.Debug(ctx, "Resume: closing resumeCh")
   315  			close(b.resumeCh)
   316  			return true
   317  		}
   318  	}
   319  	return false
   320  }
   321  
   322  func (b *BackgroundConvLoader) isSuspended() bool {
   323  	b.Lock()
   324  	defer b.Unlock()
   325  	return b.suspendCount > 0
   326  }
   327  
   328  func (b *BackgroundConvLoader) isRunning() bool {
   329  	b.Lock()
   330  	defer b.Unlock()
   331  	return b.started
   332  }
   333  
   334  func (b *BackgroundConvLoader) enqueue(ctx context.Context, task clTask) error {
   335  	b.Lock()
   336  	defer b.Unlock()
   337  	b.Debug(ctx, "enqueue: adding task: %s", task.job)
   338  	queued, err := b.queue.Push(task)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	if !queued {
   343  		b.Debug(ctx, "enqueue: skipped queueing job: %s", task.job)
   344  	}
   345  	return nil
   346  }
   347  
   348  func (b *BackgroundConvLoader) loop(uid gregor1.UID, stopCh chan struct{}) error {
   349  	bgctx := context.Background()
   350  	b.Debug(bgctx, "loop: starting conv loader loop for %s", uid)
   351  
   352  	// waitForResume is called on suspend. It will wait for a resume event, and then pause
   353  	// for b.resumeWait amount of time. Returns false if the outer loop should shutdown.
   354  	waitForResume := func(ch chan struct{}) bool {
   355  		b.Debug(bgctx, "waitForResume: suspending loop")
   356  		select {
   357  		case <-ch:
   358  		case <-stopCh:
   359  			return false
   360  		}
   361  		b.clock.Sleep(libkb.RandomJitter(b.resumeWait))
   362  		b.Debug(bgctx, "waitForResume: resuming loop")
   363  		return true
   364  	}
   365  	// On mobile fresh start, apply the foreground wait
   366  	if b.G().IsMobileAppType() {
   367  		b.Debug(bgctx, "loop: delaying startup since on mobile")
   368  		b.clock.Sleep(libkb.RandomJitter(b.resumeWait))
   369  	}
   370  
   371  	// Main loop
   372  	for {
   373  		b.Debug(bgctx, "loop: waiting for job")
   374  		select {
   375  		case <-b.queue.Wait():
   376  			task, ok := b.queue.PopFront()
   377  			if !ok {
   378  				continue
   379  			}
   380  			if task.job.ConvID.IsNil() {
   381  				// means we closed this channel
   382  				continue
   383  			}
   384  			// Wait for a small amount of time before loading, this way we aren't in a tight loop
   385  			// charging through conversations
   386  			duration := bgLoaderInitDelay
   387  			if task.attempt > 0 {
   388  				duration = bgLoaderErrDelay - time.Since(task.lastAttemptAt)
   389  				if duration < bgLoaderInitDelay {
   390  					duration = bgLoaderInitDelay
   391  				}
   392  			}
   393  			// Make sure we aren't suspended (also make sure we don't get shutdown). Charge through if
   394  			// neither have any data on them.
   395  			select {
   396  			case <-b.clock.After(duration):
   397  			case ch := <-b.suspendCh:
   398  				b.Debug(bgctx, "loop: pulled queue task, but suspended, so waiting")
   399  				if !waitForResume(ch) {
   400  					return nil
   401  				}
   402  			}
   403  			b.Debug(bgctx, "loop: pulled queued task: %s", task.job)
   404  			select {
   405  			case b.loadCh <- &task:
   406  			default:
   407  				b.Debug(bgctx, "loop: failed to dispatch load, queue full")
   408  			}
   409  		case ch := <-b.suspendCh:
   410  			b.Debug(bgctx, "loop: received suspend")
   411  			if !waitForResume(ch) {
   412  				return nil
   413  			}
   414  		case <-stopCh:
   415  			b.Debug(bgctx, "loop: shutting down for %s", uid)
   416  			return nil
   417  		}
   418  	}
   419  }
   420  
   421  func (b *BackgroundConvLoader) loadLoop(uid gregor1.UID, stopCh chan struct{}) error {
   422  	bgctx := context.Background()
   423  	b.Debug(bgctx, "loadLoop: starting for uid: %s", uid)
   424  	for {
   425  		select {
   426  		case task := <-b.loadCh:
   427  			switch {
   428  			case !b.isRunning():
   429  				b.Debug(bgctx, "loadLoop: shutting down for %s", uid)
   430  				return nil
   431  			case b.isSuspended():
   432  				b.Debug(bgctx, "loadLoop: suspended, re-enqueueing task: %s", task.job)
   433  				if err := b.enqueue(bgctx, *task); err != nil {
   434  					b.Debug(bgctx, "enqueue error %s", err)
   435  				}
   436  			default:
   437  				b.Debug(bgctx, "loadLoop: running task: %s", task.job)
   438  				nextTask := b.load(bgctx, *task, uid)
   439  				if nextTask != nil {
   440  					if err := b.enqueue(bgctx, *nextTask); err != nil {
   441  						b.Debug(bgctx, "enqueue error %s", err)
   442  					}
   443  				}
   444  			}
   445  			b.clock.Sleep(b.loadWait)
   446  		case <-stopCh:
   447  			b.Debug(bgctx, "loadLoop: shutting down for %s", uid)
   448  			return nil
   449  		}
   450  	}
   451  }
   452  
   453  func (b *BackgroundConvLoader) newQueue() {
   454  	b.queue = newJobQueue(1000)
   455  	b.loadCh = make(chan *clTask, 100)
   456  }
   457  
   458  func (b *BackgroundConvLoader) retriableError(err error) bool {
   459  	if IsOfflineError(err) != OfflineErrorKindOnline {
   460  		return true
   461  	}
   462  	if err == context.Canceled {
   463  		return true
   464  	}
   465  	switch err.(type) {
   466  	case storage.AbortedError:
   467  		return true
   468  	default:
   469  		return false
   470  	}
   471  }
   472  
   473  func (b *BackgroundConvLoader) IsBackgroundActive() bool {
   474  	b.Lock()
   475  	defer b.Unlock()
   476  	return len(b.activeLoads) > 0
   477  }
   478  
   479  func (b *BackgroundConvLoader) load(ictx context.Context, task clTask, uid gregor1.UID) *clTask {
   480  	defer b.Trace(ictx, nil, "load: %s", task.job)()
   481  	defer b.PerfTrace(ictx, nil, "load: %s", task.job)()
   482  	b.Lock()
   483  	var al activeLoad
   484  	al.Ctx, al.CancelFn = context.WithCancel(
   485  		globals.ChatCtx(b.makeConvLoaderContext(ictx), b.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil,
   486  			b.identNotifier))
   487  	ctx := al.Ctx
   488  	alKey := b.addActiveLoadLocked(al)
   489  	b.Unlock()
   490  	if b.testingNameInfoSource != nil {
   491  		ctx = globals.CtxAddOverrideNameInfoSource(ctx, b.testingNameInfoSource)
   492  		b.Debug(ctx, "setting testing nameinfo source: %T", b.testingNameInfoSource)
   493  	}
   494  	defer func() {
   495  		b.Lock()
   496  		b.removeActiveLoadLocked(alKey)
   497  		al.CancelFn()
   498  		b.Unlock()
   499  	}()
   500  
   501  	job := task.job
   502  	query := &chat1.GetThreadQuery{MarkAsRead: false}
   503  	pagination := job.Pagination
   504  	if pagination == nil {
   505  		pagination = &chat1.Pagination{Num: 50}
   506  	}
   507  	var tv chat1.ThreadView
   508  	if pagination.Num > 0 {
   509  		var err error
   510  		tv, err = b.G().ConvSource.Pull(ctx, job.ConvID, uid,
   511  			chat1.GetThreadReason_BACKGROUNDCONVLOAD, nil, query, pagination)
   512  		if err != nil {
   513  			b.Debug(ctx, "load: ConvSource.Pull error: %s (%T)", err, err)
   514  			if b.retriableError(err) && task.attempt+1 < bgLoaderMaxAttempts {
   515  				b.Debug(ctx, "transient error, retrying")
   516  				task.attempt++
   517  				task.lastAttemptAt = time.Now()
   518  				return &task
   519  			}
   520  			b.Debug(ctx, "load: failed to load job: %s", job)
   521  			return nil
   522  		}
   523  		b.Debug(ctx, "load: loaded job: %s", job)
   524  	} else {
   525  		b.Debug(ctx, "load: skipped job load because of 0 pagination")
   526  	}
   527  	if job.PostLoadHook != nil {
   528  		b.Debug(ctx, "load: invoking post load hook on job: %s", job)
   529  		job.PostLoadHook(ctx, tv, job)
   530  	}
   531  
   532  	// if testing, put the convID on the loads channel
   533  	if b.loads != nil {
   534  		b.Debug(ctx, "load: putting convID %s on loads chan", job.ConvID)
   535  		b.loads <- job.ConvID
   536  	}
   537  	return nil
   538  }
   539  
   540  func newConvLoaderPagebackHook(g *globals.Context, curCalls, maxCalls int) func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) {
   541  	return func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) {
   542  		if curCalls >= maxCalls || tv.Pagination == nil || tv.Pagination.Last {
   543  			g.GetLog().CDebugf(ctx, "newConvLoaderPagebackHook: bailing out: job: %s curcalls: %d p: %s",
   544  				job, curCalls, tv.Pagination)
   545  			return
   546  		}
   547  		job.Pagination.Next = tv.Pagination.Next
   548  		job.Pagination.Previous = nil
   549  		job.Priority = types.ConvLoaderPriorityLow
   550  		job.PostLoadHook = newConvLoaderPagebackHook(g, curCalls+1, maxCalls)
   551  		// Create a new context here so that we don't trip conv loader blocking rule
   552  		ctx = globals.BackgroundChatCtx(ctx, g)
   553  		if err := g.ConvLoader.Queue(ctx, job); err != nil {
   554  			g.GetLog().CDebugf(ctx, "newConvLoaderPagebackHook: failed to queue job: job: %s err: %s",
   555  				job, err)
   556  		}
   557  	}
   558  }